internal/godoc: clean up use of templates

- convert from text/template to html/template
- use proper template set
- always pass *godoc.Page to templates, with custom value in .Data
- move stateful site template functions to methods on *godoc.Page
- unexport Presentation.ServeFile: ServeHTTP is good enough
- reorder api.DB.Func args to match source order (pkg first)
- rename lib/godoc/godoc.html to lib/godoc/site.html
  (lib/godoc itself must stay lib/godoc because of links to other content it holds).

Change-Id: I873f17db20107fdab11d276932e6d847a6081015
Reviewed-on: https://go-review.googlesource.com/c/website/+/317655
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/_content/doc/articles/race_detector.html b/_content/doc/articles/race_detector.html
index 09188c1..01278a2 100644
--- a/_content/doc/articles/race_detector.html
+++ b/_content/doc/articles/race_detector.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Data Race Detector",
-	"Template": true
+	"Title": "Data Race Detector"
 }-->
 
 <h2 id="Introduction">Introduction</h2>
diff --git a/_content/doc/code.html b/_content/doc/code.html
index 99a1729..1f020c9 100644
--- a/_content/doc/code.html
+++ b/_content/doc/code.html
@@ -183,7 +183,7 @@
 <pre>
 # Windows users should consult https://github.com/golang/go/wiki/SettingGOPATH
 # for setting %PATH%.
-$ <b>export PATH=$PATH:$(dirname $(go list -f '{{"{{"}}.Target{{"}}"}}' .))</b>
+$ <b>export PATH=$PATH:$(dirname $(go list -f '{{"{{.Target}}"}}' .))</b>
 $ <b>hello</b>
 Hello, world.
 $
diff --git a/_content/doc/diagnostics.html b/_content/doc/diagnostics.html
index 438cdce..c97140a 100644
--- a/_content/doc/diagnostics.html
+++ b/_content/doc/diagnostics.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Diagnostics",
-	"Template": true
+	"Title": "Diagnostics"
 }-->
 
 <!--
diff --git a/_content/doc/editors.html b/_content/doc/editors.html
index e0d0c53..0ba8836 100644
--- a/_content/doc/editors.html
+++ b/_content/doc/editors.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Editor plugins and IDEs",
-	"Template": true
+	"Title": "Editor plugins and IDEs"
 }-->
 
 <h2 id="introduction">Introduction</h2>
diff --git a/_content/doc/effective_go.html b/_content/doc/effective_go.html
index 3db4d1b..f2f056d 100644
--- a/_content/doc/effective_go.html
+++ b/_content/doc/effective_go.html
@@ -3638,13 +3638,13 @@
 form value.
 Within the template text (<code>templateStr</code>),
 double-brace-delimited pieces denote template actions.
-The piece from <code>{{html "{{if .}}"}}</code>
-to <code>{{html "{{end}}"}}</code> executes only if the value of the current data item, called <code>.</code> (dot),
+The piece from <code>{{"{{if .}}"}}</code>
+to <code>{{"{{end}}"}}</code> executes only if the value of the current data item, called <code>.</code> (dot),
 is non-empty.
 That is, when the string is empty, this piece of the template is suppressed.
 </p>
 <p>
-The two snippets <code>{{html "{{.}}"}}</code> say to show the data presented to
+The two snippets <code>{{"{{.}}"}}</code> say to show the data presented to
 the template—the query string—on the web page.
 The HTML template package automatically provides appropriate escaping so the
 text is safe to display.
diff --git a/_content/doc/go1.1.html b/_content/doc/go1.1.html
index a1130c0..edda047 100644
--- a/_content/doc/go1.1.html
+++ b/_content/doc/go1.1.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.1 Release Notes",
-	"Template": true
+	"Title": "Go 1.1 Release Notes"
 }-->
 
 <h2 id="introduction">Introduction to Go 1.1</h2>
diff --git a/_content/doc/go1.10.html b/_content/doc/go1.10.html
index 0445026..5201e4a 100644
--- a/_content/doc/go1.10.html
+++ b/_content/doc/go1.10.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.10 Release Notes",
-	"Template": true
+	"Title": "Go 1.10 Release Notes"
 }-->
 
 <!--
diff --git a/_content/doc/go1.11.html b/_content/doc/go1.11.html
index fad1e0d..b513890 100644
--- a/_content/doc/go1.11.html
+++ b/_content/doc/go1.11.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.11 Release Notes",
-	"Template": true
+	"Title": "Go 1.11 Release Notes"
 }-->
 
 <!--
@@ -907,11 +906,11 @@
       Modifying template variables via assignments is now permitted via the <code>=</code> token:
     </p>
     <pre>
-  {{"{{"}} $v := "init" {{"}}"}}
-  {{"{{"}} if true {{"}}"}}
-    {{"{{"}} $v = "changed" {{"}}"}}
-  {{"{{"}} end {{"}}"}}
-  v: {{"{{"}} $v {{"}}"}} {{"{{"}}/* "changed" */{{"}}"}}</pre>
+  {{ $v := "init" }}
+  {{ if true }}
+    {{ $v = "changed" }}
+  {{ end }}
+  v: {{ $v }} {{/* "changed" */}}</pre>
 
     <p><!-- CL 95215 -->
       In previous versions untyped <code>nil</code> values passed to
diff --git a/_content/doc/go1.12.html b/_content/doc/go1.12.html
index c3181e0..21d77f1 100644
--- a/_content/doc/go1.12.html
+++ b/_content/doc/go1.12.html
@@ -1,6 +1,5 @@
 <!--{
-        "Title": "Go 1.12 Release Notes",
-        "Template": true
+        "Title": "Go 1.12 Release Notes"
 }-->
 
 <!--
diff --git a/_content/doc/go1.13.html b/_content/doc/go1.13.html
index 6bbc84e..af1d8bb 100644
--- a/_content/doc/go1.13.html
+++ b/_content/doc/go1.13.html
@@ -1,6 +1,5 @@
 <!--{
-        "Title": "Go 1.13 Release Notes",
-        "Template": true
+        "Title": "Go 1.13 Release Notes"
 }-->
 
 <!--
diff --git a/_content/doc/go1.2.html b/_content/doc/go1.2.html
index acc4436..aa05934 100644
--- a/_content/doc/go1.2.html
+++ b/_content/doc/go1.2.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.2 Release Notes",
-	"Template": true
+	"Title": "Go 1.2 Release Notes"
 }-->
 
 <h2 id="introduction">Introduction to Go 1.2</h2>
@@ -554,7 +553,7 @@
 </p>
 
 <pre>
-{{"{{"}}if eq .A 1 2 3 {{"}}"}} equal {{"{{"}}else{{"}}"}} not equal {{"{{"}}end{{"}}"}}
+{{if eq .A 1 2 3}} equal {{else}} not equal {{end}}
 </pre>
 
 <p>
@@ -567,7 +566,7 @@
 </p>
 
 <pre>
-{{"{{"}}if eq .A 1{{"}}"}} X {{"{{"}}else{{"}}"}} {{"{{"}}if eq .A 2{{"}}"}} Y {{"{{"}}end{{"}}"}} {{"{{"}}end{{"}}"}}
+{{if eq .A 1}} X {{else}} {{if eq .A 2}} Y {{end}} {{end}}
 </pre>
 
 <p>
@@ -575,7 +574,7 @@
 </p>
 
 <pre>
-{{"{{"}}if eq .A 1{{"}}"}} X {{"{{"}}else if eq .A 2{{"}}"}} Y {{"{{"}}end{{"}}"}}
+{{if eq .A 1}} X {{else if eq .A 2}} Y {{end}}
 </pre>
 
 <p>
diff --git a/_content/doc/go1.3.html b/_content/doc/go1.3.html
index 57d1d7f..8edb6ee 100644
--- a/_content/doc/go1.3.html
+++ b/_content/doc/go1.3.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.3 Release Notes",
-	"Template": true
+	"Title": "Go 1.3 Release Notes"
 }-->
 
 <h2 id="introduction">Introduction to Go 1.3</h2>
diff --git a/_content/doc/go1.4.html b/_content/doc/go1.4.html
index 0233ad4..1c306f6 100644
--- a/_content/doc/go1.4.html
+++ b/_content/doc/go1.4.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.4 Release Notes",
-	"Template": true
+	"Title": "Go 1.4 Release Notes"
 }-->
 
 <h2 id="introduction">Introduction to Go 1.4</h2>
diff --git a/_content/doc/go1.5.html b/_content/doc/go1.5.html
index d9529f2..bd7517c 100644
--- a/_content/doc/go1.5.html
+++ b/_content/doc/go1.5.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.5 Release Notes",
-	"Template": true
+	"Title": "Go 1.5 Release Notes"
 }-->
 
 
diff --git a/_content/doc/go1.6.html b/_content/doc/go1.6.html
index 2f7093d..edf89d2 100644
--- a/_content/doc/go1.6.html
+++ b/_content/doc/go1.6.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.6 Release Notes",
-	"Template": true
+	"Title": "Go 1.6 Release Notes"
 }-->
 
 <!--
@@ -443,9 +442,9 @@
 </p>
 
 <pre>
-{{"{{"}}23 -}}
+{{23 -}}
    &lt;
-{{"{{"}}- 45}}
+{{- 45}}
 </pre>
 
 <p>
@@ -453,7 +452,7 @@
 </p>
 
 <p>
-Second, the new <a href="/pkg/text/template/#hdr-Actions"><code>{{"{{"}}block}}</code> action</a>,
+Second, the new <a href="/pkg/text/template/#hdr-Actions"><code>{{block}}</code> action</a>,
 combined with allowing redefinition of named templates,
 provides a simple way to define pieces of a template that
 can be replaced in different instantiations.
diff --git a/_content/doc/go1.7.html b/_content/doc/go1.7.html
index 2138871..79236d6 100644
--- a/_content/doc/go1.7.html
+++ b/_content/doc/go1.7.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.7 Release Notes",
-	"Template": true
+	"Title": "Go 1.7 Release Notes"
 }-->
 
 <!--
diff --git a/_content/doc/go1.8.html b/_content/doc/go1.8.html
index adb8991..56290f0 100644
--- a/_content/doc/go1.8.html
+++ b/_content/doc/go1.8.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.8 Release Notes",
-	"Template": true
+	"Title": "Go 1.8 Release Notes"
 }-->
 
 <!--
diff --git a/_content/doc/go1.9.html b/_content/doc/go1.9.html
index ddd310f..374ac35 100644
--- a/_content/doc/go1.9.html
+++ b/_content/doc/go1.9.html
@@ -1,6 +1,5 @@
 <!--{
-	"Title": "Go 1.9 Release Notes",
-	"Template": true
+	"Title": "Go 1.9 Release Notes"
 }-->
 
 <!--
diff --git a/_content/lib/godoc/codewalk.html b/_content/lib/godoc/codewalk.html
index 0f3d22a..89360bf 100644
--- a/_content/lib/godoc/codewalk.html
+++ b/_content/lib/godoc/codewalk.html
@@ -4,6 +4,7 @@
 	license that can be found in the LICENSE file.
 -->
 
+{{with .Data}}
 <style type='text/css'>@import "/doc/codewalk/codewalk.css";</style>
 <script type="text/javascript" src="/doc/codewalk/codewalk.js"></script>
 
@@ -17,7 +18,7 @@
         </a>
         <select id="code-selector">
           {{range .File}}
-          <option value="/doc/codewalk/?fileprint=/{{urlquery .}}">{{html .}}</option>
+          <option value="/doc/codewalk/?fileprint=/{{.}}">{{.}}</option>
           {{end}}
         </select>
       </div>
@@ -35,15 +36,15 @@
     <div id="comment-area">
       {{range .Step}}
       <div class="comment first last">
-        <a class="comment-link" href="/doc/codewalk/?fileprint=/{{urlquery .File}}&amp;lo={{urlquery .Lo}}&amp;hi={{urlquery .Hi}}#mark" target="code-display"></a>
-        <div class="comment-title">{{html .Title}}</div>
+        <a class="comment-link" href="/doc/codewalk/?fileprint=/{{.File}}&amp;lo={{.Lo}}&amp;hi={{.Hi}}#mark" target="code-display"></a>
+        <div class="comment-title">{{.Title}}</div>
         <div class="comment-text">
 	{{with .Err}}
-	ERROR LOADING FILE: {{html .}}<br/><br/>
+	ERROR LOADING FILE: {{.}}<br/><br/>
 	{{end}}
-        {{.XML}}
+        {{.HTML}}
         </div>
-        <div class="comment-text file-name"><span class="path-file">{{html .}}</span></div>
+        <div class="comment-text file-name"><span class="path-file">{{.}}</span></div>
       </div>
       {{end}}
     </div>
@@ -54,3 +55,4 @@
     </div>
   </div>
 </div>
+{{end}}
diff --git a/_content/lib/godoc/codewalkdir.html b/_content/lib/godoc/codewalkdir.html
index b7674c6..5479429 100644
--- a/_content/lib/godoc/codewalkdir.html
+++ b/_content/lib/godoc/codewalkdir.html
@@ -5,12 +5,11 @@
 -->
 
 <table class="layout">
-{{range .}}
+{{range .Data}}
 <tr>
-	{{$name_html := html .Name}}
-	<td><a href="{{$name_html}}">{{$name_html}}</a></td>
+	<td><a href="{{.Name}}">{{.Name}}</a></td>
 	<td width="25">&nbsp;</td>
-	<td>{{html .Title}}</td>
+	<td>{{.Title}}</td>
 </tr>
 {{end}}
 </table>
diff --git a/_content/lib/godoc/dirlist.html b/_content/lib/godoc/dirlist.html
index 78530e6..6f32403 100644
--- a/_content/lib/godoc/dirlist.html
+++ b/_content/lib/godoc/dirlist.html
@@ -4,6 +4,7 @@
 	license that can be found in the LICENSE file.
 -->
 
+{{with .Data}}
 <p>
 <table class="layout">
 <tr>
@@ -15,11 +16,12 @@
 	<td><a href="../">../</a></td>
 </tr>
 {{range .}}{{if .IsDir}}
-	<tr><td align="left"><a href="{{html .Name}}/">{{html .Name}}/</a><td></tr>
+	<tr><td align="left"><a href="{{.Name}}/">{{.Name}}/</a><td></tr>
 {{end}}{{end}}
 {{range .}}{{if not .IsDir}}
-	<tr><td align="left"><a href="{{html .Name}}">{{html .Name}}</a><td align="right">{{html .Size}}</tr>
+	<tr><td align="left"><a href="{{.Name}}">{{.Name}}</a><td align="right">{{.Size}}</tr>
 {{end}}{{end}}
 
 </table>
 </p>
+{{end}}
diff --git a/_content/lib/godoc/error.html b/_content/lib/godoc/error.html
index 7573aa2..cb5325e 100644
--- a/_content/lib/godoc/error.html
+++ b/_content/lib/godoc/error.html
@@ -4,6 +4,8 @@
 	license that can be found in the LICENSE file.
 -->
 
+{{with .Data}}
 <p>
-<span class="alert" style="font-size:120%">{{html .}}</span>
+<span class="alert" style="font-size:120%">{{.}}</span>
 </p>
+{{end}}
diff --git a/_content/lib/godoc/example.html b/_content/lib/godoc/example.html
index bde02c6..aef6edb 100644
--- a/_content/lib/godoc/example.html
+++ b/_content/lib/godoc/example.html
@@ -1,14 +1,15 @@
+{{with .Data}}
 <div id="example_{{.Name}}" class="toggle">
   <div class="collapsed">
     <p class="exampleHeading toggleButton">▹ <span class="text">Example{{example_suffix .Name}}</span></p>
   </div>
   <div class="expanded">
     <p class="exampleHeading toggleButton">▾ <span class="text">Example{{example_suffix .Name}}</span></p>
-    {{with .Doc}}<p>{{html .}}</p>{{end}}
+    {{with .Doc}}<p>{{.}}</p>{{end}}
     {{$output := .Output}}
     {{with .Play}}
       <div class="play">
-        <div class="input"><textarea class="code" spellcheck="false">{{html .}}</textarea></div>
+        <div class="input"><textarea class="code" spellcheck="false">{{.}}</textarea></div>
         <div class="output"><pre>{{html $output}}</pre></div>
         <div class="buttons">
           <button class="Button Button--primary run" title="Run this code [shift-enter]">Run</button>
@@ -23,8 +24,9 @@
       <pre class="code">{{.Code}}</pre>
       {{with .Output}}
         <p>Output:</p>
-        <pre class="output">{{html .}}</pre>
+        <pre class="output">{{.}}</pre>
       {{end}}
     {{end}}
   </div>
 </div>
+{{end}}
diff --git a/_content/lib/godoc/godocs.js b/_content/lib/godoc/godocs.js
index 68e2a89..e581cb3 100644
--- a/_content/lib/godoc/godocs.js
+++ b/_content/lib/godoc/godocs.js
@@ -372,7 +372,7 @@
     personalizeInstallInstructions();
     updateVersionTags();
 
-    // godoc.html defines window.initFuncs in the <head> tag, and root.html and
+    // site.html defines window.initFuncs in the <head> tag, and root.html and
     // codewalk.js push their on-page-ready functions to the list.
     // We execute those functions here, to avoid loading jQuery until the page
     // content is loaded.
diff --git a/_content/lib/godoc/package.html b/_content/lib/godoc/package.html
index 9f0c879..3d6e8ba 100644
--- a/_content/lib/godoc/package.html
+++ b/_content/lib/godoc/package.html
@@ -9,23 +9,24 @@
 	them to conflict with generated attributes (some of which
 	correspond to Go identifiers).
 -->
-{{with .PDoc}}
-	{{if $.IsMain}}
+{{$pkg := .Data}}
+{{with $pkg.PDoc}}
+	{{if $pkg.IsMain}}
 		{{/* command documentation */}}
-		{{comment_html .Doc}}
+		{{$.Comment .Doc}}
 	{{else}}
 		{{/* package documentation */}}
 		<div id="short-nav">
 			<dl>
-			<dd><code>import "{{html .ImportPath}}"</code></dd>
+			<dd><code>import "{{.ImportPath}}"</code></dd>
 			</dl>
 			<dl>
 			<dd><a href="#pkg-overview" class="overviewLink">Overview</a></dd>
 			<dd><a href="#pkg-index" class="indexLink">Index</a></dd>
-			{{if $.Examples}}
+			{{if $pkg.Examples}}
 				<dd><a href="#pkg-examples" class="examplesLink">Examples</a></dd>
 			{{end}}
-			{{if $.Dirs}}
+			{{if $pkg.Dirs}}
 				<dd><a href="#pkg-subdirectories">Subdirectories</a></dd>
 			{{end}}
 			</dl>
@@ -37,8 +38,8 @@
 			</div>
 			<div class="expanded">
 				<h2 class="toggleButton" title="Click to hide Overview section">Overview ▾</h2>
-				{{comment_html .Doc}}
-				{{example_html $ ""}}
+				{{$.Comment .Doc}}
+				{{$.Example ""}}
 			</div>
 		</div>
 
@@ -59,33 +60,30 @@
 				<dd><a href="#pkg-variables">Variables</a></dd>
 			{{end}}
 			{{range .Funcs}}
-				{{$name_html := html .Name}}
-				<dd><a href="#{{$name_html}}">{{node_html $ .Decl false | sanitize}}</a></dd>
+				<dd><a href="#{{.Name}}">{{$.NodeTOC .Decl}}</a></dd>
 			{{end}}
 			{{range .Types}}
-				{{$tname_html := html .Name}}
-				<dd><a href="#{{$tname_html}}">type {{$tname_html}}</a></dd>
+				{{$typeName := .Name}}
+				<dd><a href="#{{.Name}}">type {{.Name}}</a></dd>
 				{{range .Funcs}}
-					{{$name_html := html .Name}}
-					<dd>&nbsp; &nbsp; <a href="#{{$name_html}}">{{node_html $ .Decl false | sanitize}}</a></dd>
+					<dd>&nbsp; &nbsp; <a href="#{{.Name}}">{{$.NodeTOC .Decl}}</a></dd>
 				{{end}}
 				{{range .Methods}}
-					{{$name_html := html .Name}}
-					<dd>&nbsp; &nbsp; <a href="#{{$tname_html}}.{{$name_html}}">{{node_html $ .Decl false | sanitize}}</a></dd>
+					<dd>&nbsp; &nbsp; <a href="#{{$typeName}}.{{.Name}}">{{$.NodeTOC .Decl}}</a></dd>
 				{{end}}
 			{{end}}
-			{{if $.Bugs}}
+			{{if $pkg.Bugs}}
 				<dd><a href="#pkg-note-BUG">Bugs</a></dd>
 			{{end}}
 			</dl>
 			</div><!-- #manual-nav -->
 
-		{{if $.Examples}}
+		{{if $pkg.Examples}}
 		<div id="pkg-examples">
 			<h3>Examples</h3>
 			<div class="js-expandAll expandAll collapsed">(Expand All)</div>
 			<dl>
-			{{range $.Examples}}
+			{{range $pkg.Examples}}
 			<dd><a class="exampleLink" href="#example_{{.Name}}">{{example_name .Name}}</a></dd>
 			{{end}}
 			</dl>
@@ -97,7 +95,7 @@
 			<p>
 			<span style="font-size:90%">
 			{{range .}}
-				<a href="{{.|srcLink|html}}">{{.|filename|html}}</a>
+				<a href="/src/{{.}}">{{basename .}}</a>
 			{{end}}
 			</span>
 			</p>
@@ -108,93 +106,87 @@
 		{{with .Consts}}
 			<h2 id="pkg-constants">Constants</h2>
 			{{range .}}
-				{{comment_html .Doc}}
-				<pre>{{node_html $ .Decl true}}</pre>
+				{{$.Comment .Doc}}
+				<pre>{{$.Node .Decl}}</pre>
 			{{end}}
 		{{end}}
 		{{with .Vars}}
 			<h2 id="pkg-variables">Variables</h2>
 			{{range .}}
-				{{comment_html .Doc}}
-				<pre>{{node_html $ .Decl true}}</pre>
+				{{$.Comment .Doc}}
+				<pre>{{$.Node .Decl}}</pre>
 			{{end}}
 		{{end}}
 		{{range .Funcs}}
 			{{/* Name is a string - no need for FSet */}}
-			{{$name_html := html .Name}}
-			<h2 id="{{$name_html}}">func <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a>
-				<a class="permalink" href="#{{$name_html}}">&#xb6;</a>
-				{{$since := since "func" "" .Name $.PDoc.ImportPath}}
+			<h2 id="{{.Name}}">func <a href="{{$.SrcPosLink .Decl}}">{{.Name}}</a>
+				<a class="permalink" href="#{{.Name}}">&#xb6;</a>
+				{{$since := $.Since "func" "" .Name}}
 				{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
 			</h2>
-			<pre>{{node_html $ .Decl true}}</pre>
-			{{comment_html .Doc}}
-			{{example_html $ .Name}}
-
+			<pre>{{$.Node .Decl}}</pre>
+			{{$.Comment .Doc}}
+			{{$.Example .Name}}
 		{{end}}
 		{{range .Types}}
-			{{$tname := .Name}}
-			{{$tname_html := html .Name}}
-			<h2 id="{{$tname_html}}">type <a href="{{posLink_url $ .Decl}}">{{$tname_html}}</a>
-				<a class="permalink" href="#{{$tname_html}}">&#xb6;</a>
-				{{$since := since "type" "" .Name $.PDoc.ImportPath}}
+			{{$typeName := .Name}}
+			<h2 id="{{.Name}}">type <a href="{{$.SrcPosLink .Decl}}">{{$typeName}}</a>
+				<a class="permalink" href="#{{.Name}}">&#xb6;</a>
+				{{$since := $.Since "type" "" .Name}}
 				{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
 			</h2>
-			{{comment_html .Doc}}
-			<pre>{{node_html $ .Decl true}}</pre>
+			{{$.Comment .Doc}}
+			<pre>{{$.Node .Decl}}</pre>
 
 			{{range .Consts}}
-				{{comment_html .Doc}}
-				<pre>{{node_html $ .Decl true}}</pre>
+				{{$.Comment .Doc}}
+				<pre>{{$.Node .Decl}}</pre>
 			{{end}}
 
 			{{range .Vars}}
-				{{comment_html .Doc}}
-				<pre>{{node_html $ .Decl true}}</pre>
+				{{$.Comment .Doc}}
+				<pre>{{$.Node .Decl}}</pre>
 			{{end}}
 
-			{{example_html $ $tname}}
+			{{$.Example .Name}}
 
 			{{range .Funcs}}
-				{{$name_html := html .Name}}
-				<h3 id="{{$name_html}}">func <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a>
-					<a class="permalink" href="#{{$name_html}}">&#xb6;</a>
-					{{$since := since "func" "" .Name $.PDoc.ImportPath}}
+				<h3 id="{{.Name}}">func <a href="{{$.SrcPosLink .Decl}}">{{.Name}}</a>
+					<a class="permalink" href="#{{.Name}}">&#xb6;</a>
+					{{$since := $.Since "func" "" .Name}}
 					{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
 				</h3>
-				<pre>{{node_html $ .Decl true}}</pre>
-				{{comment_html .Doc}}
-				{{example_html $ .Name}}
+				<pre>{{$.Node .Decl}}</pre>
+				{{$.Comment .Doc}}
+				{{$.Example .Name}}
 			{{end}}
 
 			{{range .Methods}}
-				{{$name_html := html .Name}}
-				<h3 id="{{$tname_html}}.{{$name_html}}">func ({{html .Recv}}) <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a>
-					<a class="permalink" href="#{{$tname_html}}.{{$name_html}}">&#xb6;</a>
-					{{$since := since "method" .Recv .Name $.PDoc.ImportPath}}
+				<h3 id="{{$typeName}}.{{.Name}}">func ({{html .Recv}}) <a href="{{$.SrcPosLink .Decl}}">{{.Name}}</a>
+					<a class="permalink" href="#{{$typeName}}.{{.Name}}">&#xb6;</a>
+					{{$since := $.Since "method" .Recv .Name}}
 					{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
 				</h3>
-				<pre>{{node_html $ .Decl true}}</pre>
-				{{comment_html .Doc}}
-				{{$name := printf "%s_%s" $tname .Name}}
-				{{example_html $ $name}}
+				<pre>{{$.Node .Decl}}</pre>
+				{{$.Comment .Doc}}
+				{{$.Example (printf "%s_%s" $typeName .Name)}}
 			{{end}}
 		{{end}}
 	{{end}}
 
-	{{with $.Bugs}}
+	{{with $pkg.Bugs}}
 		<h2 id="pkg-note-BUG">Bugs</h2>
 		<ul style="list-style: none; padding: 0;">
 		{{range .}}
-		<li><a href="{{posLink_url $ .}}" style="float: left;">&#x261e;</a> {{comment_html .Body}}</li>
+		<li><a href="{{$.SrcPosLink .}}" style="float: left;">&#x261e;</a> {{$.Comment .Body}}</li>
 		{{end}}
 		</ul>
 	{{end}}
 {{end}}
 
-{{with .Dirs}}
+{{with $pkg.Dirs}}
 	{{/* DirList entries are numbers and strings - no need for FSet */}}
-	{{if $.PDoc}}
+	{{if $pkg.PDoc}}
 		<h2 id="pkg-subdirectories">Subdirectories</h2>
 	{{end}}
 	<div class="pkg-dir">
@@ -204,7 +196,7 @@
 				<th class="pkg-synopsis">Synopsis</th>
 			</tr>
 
-			{{if not (or (eq $.Dirname "/src/cmd") $.DirFlat)}}
+			{{if not (or (eq $pkg.Dirname "/src/cmd") $pkg.DirFlat)}}
 			<tr>
 				<td colspan="2"><a href="..">..</a></td>
 			</tr>
@@ -212,19 +204,19 @@
 
 			{{range .List}}
 				<tr>
-				{{if $.DirFlat}}
+				{{if $pkg.DirFlat}}
 					{{if .HasPkg}}
 						<td class="pkg-name">
-							<a href="{{html .Path}}/{{modeQueryString $.Mode | html}}">{{html .Path}}</a>
+							<a href="{{.Path}}/{{$.ModeQuery}}">{{.Path}}</a>
 						</td>
 					{{end}}
 				{{else}}
 					<td class="pkg-name" style="padding-left: {{multiply .Depth 20}}px;">
-						<a href="{{html .Path}}/{{modeQueryString $.Mode | html}}">{{html .Name}}</a>
+						<a href="{{.Path}}/{{$.ModeQuery}}">{{.Name}}</a>
 					</td>
 				{{end}}
 					<td class="pkg-synopsis">
-						{{html .Synopsis}}
+						{{.Synopsis}}
 					</td>
 				</tr>
 			{{end}}
diff --git a/_content/lib/godoc/packageroot.html b/_content/lib/godoc/packageroot.html
index 5727926..218c845 100644
--- a/_content/lib/godoc/packageroot.html
+++ b/_content/lib/godoc/packageroot.html
@@ -9,10 +9,11 @@
 	them to conflict with generated attributes (some of which
 	correspond to Go identifiers).
 -->
+{{$pkg := .Data}}
 
-{{with .Dirs}}
+{{with $pkg.Dirs}}
 	{{/* DirList entries are numbers and strings - no need for FSet */}}
-	{{if $.PDoc}}
+	{{if $pkg.PDoc}}
 		<h2 id="pkg-subdirectories">Subdirectories</h2>
 	{{end}}
 		<div id="manual-nav">
@@ -40,19 +41,19 @@
 
 						{{range .List}}
 							<tr>
-							{{if $.DirFlat}}
+							{{if $pkg.DirFlat}}
 								{{if .HasPkg}}
 										<td class="pkg-name">
-											<a href="{{html .Path}}/{{modeQueryString $.Mode | html}}">{{html .Path}}</a>
+											<a href="{{.Path}}/{{$.ModeQuery}}">{{.Path}}</a>
 										</td>
 								{{end}}
 							{{else}}
 									<td class="pkg-name" style="padding-left: {{multiply .Depth 20}}px;">
-										<a href="{{html .Path}}/{{modeQueryString $.Mode | html}}">{{html .Name}}</a>
+										<a href="{{.Path}}/{{$.ModeQuery}}">{{.Name}}</a>
 									</td>
 							{{end}}
 							<td class="pkg-synopsis">
-								{{html .Synopsis}}
+								{{.Synopsis}}
 							</td>
 							</tr>
 						{{end}}
diff --git a/_content/lib/godoc/godoc.html b/_content/lib/godoc/site.html
similarity index 93%
rename from _content/lib/godoc/godoc.html
rename to _content/lib/godoc/site.html
index f1d14e4..2f3e496 100644
--- a/_content/lib/godoc/godoc.html
+++ b/_content/lib/godoc/site.html
@@ -4,8 +4,8 @@
 <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}}
-  <title>{{html .}} - The Go Programming Language</title>
+{{with .TabTitle}}
+  <title>{{.}} - The Go Programming Language</title>
 {{else}}
   <title>The Go Programming Language</title>
 {{end}}
@@ -63,18 +63,18 @@
 
 {{if or .Title .SrcPath}}
   <h1>
-    {{html .Title}}
-    {{html .SrcPath | srcBreadcrumb}}
+    {{.Title}}
+    {{$.SrcBreadcrumb}}
   </h1>
 {{end}}
 
 {{with .Subtitle}}
-  <h2>{{html .}}</h2>
+  <h2>{{.}}</h2>
 {{end}}
 
 {{with .SrcPath}}
   <h2>
-    Documentation: {{html . | srcToPkgLink}}
+    Documentation: {{$.SrcPkgLink}}
   </h2>
 {{end}}
 
@@ -82,8 +82,11 @@
      Do not delete this <div>. */}}
 <div id="nav"></div>
 
-{{/* Body is HTML-escaped elsewhere */}}
-{{printf "%s" .Body}}
+{{if .Template}}
+{{.Invoke .Template .Data}}
+{{else}}
+{{.Data}}
+{{end}}
 
 </div><!-- .container -->
 </main><!-- #page -->
@@ -108,4 +111,3 @@
 })();
 </script>
 {{end}}
-
diff --git a/cmd/golangorg/codewalk.go b/cmd/golangorg/codewalk.go
index b571343..d8d2284 100644
--- a/cmd/golangorg/codewalk.go
+++ b/cmd/golangorg/codewalk.go
@@ -16,10 +16,10 @@
 package main
 
 import (
-	"bytes"
 	"encoding/xml"
 	"errors"
 	"fmt"
+	"html/template"
 	"io"
 	"io/fs"
 	"log"
@@ -30,14 +30,11 @@
 	"sort"
 	"strconv"
 	"strings"
-	"text/template"
 	"unicode/utf8"
 
 	"golang.org/x/website/internal/godoc"
 )
 
-var codewalkHTML, codewalkdirHTML *template.Template
-
 // Handler for /doc/codewalk/ and below.
 func codewalk(w http.ResponseWriter, r *http.Request) {
 	relpath := r.URL.Path[len("/doc/codewalk/"):]
@@ -58,7 +55,7 @@
 
 	// If file exists, serve using standard file server.
 	if err == nil {
-		pres.ServeFile(w, r)
+		pres.ServeHTTP(w, r)
 		return
 	}
 
@@ -69,7 +66,7 @@
 	cw, err := loadCodewalk(abspath + ".xml")
 	if err != nil {
 		log.Print(err)
-		pres.ServeError(w, r, relpath, err)
+		pres.ServeError(w, r, err)
 		return
 	}
 
@@ -78,10 +75,11 @@
 		return
 	}
 
-	pres.ServePage(w, godoc.Page{
+	pres.ServePage(w, r, godoc.Page{
 		Title:    "Codewalk: " + cw.Title,
-		Tabtitle: cw.Title,
-		Body:     applyTemplate(codewalkHTML, "codewalk", cw),
+		TabTitle: cw.Title,
+		Template: "codewalk.html",
+		Data:     cw,
 	})
 }
 
@@ -99,14 +97,6 @@
 	return
 }
 
-func applyTemplate(t *template.Template, name string, data interface{}) []byte {
-	var buf bytes.Buffer
-	if err := t.Execute(&buf, data); err != nil {
-		log.Printf("%s.Execute: %s", name, err)
-	}
-	return buf.Bytes()
-}
-
 // A Codewalk represents a single codewalk read from an XML file.
 type Codewalk struct {
 	Title string      `xml:"title,attr"`
@@ -131,6 +121,10 @@
 	Data   []byte
 }
 
+func (c *Codestep) HTML() template.HTML {
+	return template.HTML(c.XML)
+}
+
 // String method for printing in template.
 // Formats file address nicely.
 func (st *Codestep) String() string {
@@ -217,7 +211,7 @@
 	dir, err := fs.ReadDir(fsys, toFS(abspath))
 	if err != nil {
 		log.Print(err)
-		pres.ServeError(w, r, relpath, err)
+		pres.ServeError(w, r, err)
 		return
 	}
 	var v []interface{}
@@ -234,9 +228,10 @@
 		}
 	}
 
-	pres.ServePage(w, godoc.Page{
-		Title: "Codewalks",
-		Body:  applyTemplate(codewalkdirHTML, "codewalkdir", v),
+	pres.ServePage(w, r, godoc.Page{
+		Title:    "Codewalks",
+		Template: "codewalkdir.html",
+		Data:     v,
 	})
 }
 
@@ -251,7 +246,7 @@
 	data, err := fs.ReadFile(fsys, toFS(abspath))
 	if err != nil {
 		log.Print(err)
-		pres.ServeError(w, r, f, err)
+		pres.ServeError(w, r, err)
 		return
 	}
 	lo, _ := strconv.Atoi(r.FormValue("lo"))
diff --git a/cmd/golangorg/handlers.go b/cmd/golangorg/handlers.go
index 28a3b80..1ee843d 100644
--- a/cmd/golangorg/handlers.go
+++ b/cmd/golangorg/handlers.go
@@ -11,7 +11,6 @@
 	"encoding/json"
 	"go/format"
 	"io/fs"
-	"log"
 	"net/http"
 	pathpkg "path"
 	"strings"
@@ -93,16 +92,6 @@
 	return mux
 }
 
-func readTemplates(p *godoc.Presentation) {
-	var err error
-	if codewalkHTML, err = p.ReadTemplate("codewalk.html"); err != nil {
-		log.Fatal(err)
-	}
-	if codewalkdirHTML, err = p.ReadTemplate("codewalkdir.html"); err != nil {
-		log.Fatal(err)
-	}
-}
-
 type fmtResponse struct {
 	Body  string
 	Error string
diff --git a/cmd/golangorg/local.go b/cmd/golangorg/local.go
index 37353b3..ba00aaa 100644
--- a/cmd/golangorg/local.go
+++ b/cmd/golangorg/local.go
@@ -27,7 +27,7 @@
 		os.Exit(2)
 	}
 	dir := filepath.Join(file, "../../../_content")
-	if _, err := os.Stat(filepath.Join(dir, "lib/godoc/godoc.html")); err != nil {
+	if _, err := os.Stat(filepath.Join(dir, "lib/godoc/site.html")); err != nil {
 		log.Printf("warning: cannot find template dir; using embedded copy")
 		return
 	}
diff --git a/cmd/golangorg/main.go b/cmd/golangorg/main.go
index 283f4e0..9c20031 100644
--- a/cmd/golangorg/main.go
+++ b/cmd/golangorg/main.go
@@ -85,7 +85,6 @@
 	}
 	pres.GoogleCN = googleCN
 
-	readTemplates(pres)
 	mux := registerHandlers(pres)
 	lateSetup(mux)
 
diff --git a/cmd/golangorg/release_test.go b/cmd/golangorg/release_test.go
index 4b19884..5c6a574 100644
--- a/cmd/golangorg/release_test.go
+++ b/cmd/golangorg/release_test.go
@@ -32,7 +32,6 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	readTemplates(pres)
 	mux := registerHandlers(pres)
 
 	req := httptest.NewRequest(http.MethodGet, "/doc/devel/release", nil)
diff --git a/internal/api/api.go b/internal/api/api.go
index a9669f2..7fcb23d 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -51,7 +51,7 @@
 //
 // The name is the symbol name ("Server") and the pkg is the package
 // ("net/http").
-func (v DB) Func(kind, receiver, name, pkg string) string {
+func (v DB) Func(pkg, kind, receiver, name string) string {
 	pv := v[pkg]
 	switch kind {
 	case "func":
diff --git a/internal/api/api_test.go b/internal/api/api_test.go
index 64c8800..ced9aa4 100644
--- a/internal/api/api_test.go
+++ b/internal/api/api_test.go
@@ -137,8 +137,8 @@
 		if tc.want != "" && !hasTag("go"+tc.want) {
 			continue
 		}
-		if got := av.Func(tc.kind, tc.receiver, tc.name, tc.pkg); got != tc.want {
-			t.Errorf(`sinceFunc("%s", "%s", "%s", "%s") = "%s"; want "%s"`, tc.kind, tc.receiver, tc.name, tc.pkg, got, tc.want)
+		if got := av.Func(tc.pkg, tc.kind, tc.receiver, tc.name); got != tc.want {
+			t.Errorf(`sinceFunc(%q, %q, %q, %q) = %q; want %q`, tc.pkg, tc.kind, tc.receiver, tc.name, got, tc.want)
 		}
 	}
 }
diff --git a/internal/godoc/astfuncs.go b/internal/godoc/astfuncs.go
index f6cafd4..e21b52a 100644
--- a/internal/godoc/astfuncs.go
+++ b/internal/godoc/astfuncs.go
@@ -14,6 +14,7 @@
 	"go/doc"
 	"go/printer"
 	"go/token"
+	"html/template"
 	"io"
 	"log"
 	"unicode"
@@ -25,26 +26,36 @@
 
 var slashSlash = []byte("//")
 
-func (p *Presentation) nodeFunc(info *pkgdoc.Page, node interface{}) string {
-	var buf bytes.Buffer
-	p.writeNode(&buf, info, info.FSet, node)
-	return buf.String()
-}
-
-func (p *Presentation) node_htmlFunc(info *pkgdoc.Page, node interface{}, linkify bool) string {
+// Node formats the given AST node as HTML.
+// Identifiers in the rendered node
+// are turned into links to their documentation.
+func (p *Page) Node(node interface{}) template.HTML {
+	info := p.Data.(*pkgdoc.Page)
 	var buf1 bytes.Buffer
-	p.writeNode(&buf1, info, info.FSet, node)
+	p.pres.writeNode(&buf1, info, info.FSet, node)
 
 	var buf2 bytes.Buffer
-	var n ast.Node
-	if linkify {
-		n, _ = node.(ast.Node)
-	}
+	n, _ := node.(ast.Node)
 	buf2.Write(texthtml.Format(buf1.Bytes(), texthtml.Config{
 		AST:        n,
 		GoComments: true,
 	}))
-	return buf2.String()
+	return template.HTML(buf2.String())
+}
+
+// NodeTOC formats the given AST node as HTML
+// for inclusion in the table of contents.
+func (p *Page) NodeTOC(node interface{}) template.HTML {
+	info := p.Data.(*pkgdoc.Page)
+	var buf1 bytes.Buffer
+	p.pres.writeNode(&buf1, info, info.FSet, node)
+
+	var buf2 bytes.Buffer
+	buf2.Write(texthtml.Format(buf1.Bytes(), texthtml.Config{
+		GoComments: true,
+	}))
+
+	return sanitize(template.HTML(buf2.String()))
 }
 
 const TabWidth = 4
@@ -140,18 +151,19 @@
 	return string(x[:i])
 }
 
-func comment_htmlFunc(comment string) string {
+// Comment formats the given documentation comment as HTML.
+func (p *Page) Comment(comment string) template.HTML {
 	var buf bytes.Buffer
 	// TODO(gri) Provide list of words (e.g. function parameters)
 	//           to be emphasized by ToHTML.
 	doc.ToHTML(&buf, comment, nil) // does html-escaping
-	return buf.String()
+	return template.HTML(buf.String())
 }
 
-// sanitizeFunc sanitizes the argument src by replacing newlines with
+// sanitize sanitizes the argument src by replacing newlines with
 // blanks, removing extra blanks, and by removing trailing whitespace
 // and commas before closing parentheses.
-func sanitizeFunc(src string) string {
+func sanitize(src template.HTML) template.HTML {
 	buf := make([]byte, len(src))
 	j := 0      // buf index
 	comma := -1 // comma index if >= 0
@@ -189,5 +201,13 @@
 	if j > 0 && buf[j-1] == ' ' {
 		j--
 	}
-	return string(buf[:j])
+	return template.HTML(buf[:j])
+}
+
+// Since reports the Go version that introduced the API feature
+// identified by kind, reeciver, name.
+// The current package is deduced from p.Data, which must be a *pkgdoc.Page.
+func (p *Page) Since(kind, receiver, name string) string {
+	pkg := p.Data.(*pkgdoc.Page).PDoc.ImportPath
+	return p.pres.api.Func(pkg, kind, receiver, name)
 }
diff --git a/internal/godoc/examplefuncs.go b/internal/godoc/examplefuncs.go
index 3d10fc1..67099ec 100644
--- a/internal/godoc/examplefuncs.go
+++ b/internal/godoc/examplefuncs.go
@@ -12,6 +12,7 @@
 	"go/ast"
 	"go/format"
 	"go/printer"
+	"html/template"
 	"log"
 	"regexp"
 	"strings"
@@ -20,7 +21,10 @@
 	"golang.org/x/website/internal/pkgdoc"
 )
 
-func (p *Presentation) example_htmlFunc(info *pkgdoc.Page, funcName string) string {
+// Example renders the examples for the given function name as HTML.
+// The current package is deduced from p.Data, which must be a *pkgdoc.Page.
+func (p *Page) Example(funcName string) template.HTML {
+	info := p.Data.(*pkgdoc.Page)
 	var buf bytes.Buffer
 	for _, eg := range info.Examples {
 		name := pkgdoc.TrimExampleSuffix(eg.Name)
@@ -31,7 +35,7 @@
 
 		// print code
 		cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
-		code := p.node_htmlFunc(info, cnode, true)
+		code := string(p.Node(cnode))
 		out := eg.Output
 		wholeFile := true
 
@@ -66,20 +70,23 @@
 			out = ""
 		}
 
-		if p.ExampleHTML == nil {
-			out = ""
+		t := p.pres.Templates.Lookup("example.html")
+		if t == nil {
 			return ""
 		}
 
-		err := p.ExampleHTML.Execute(&buf, struct {
+		newPage := *p
+		newPage.Data = struct {
 			Name, Doc, Code, Play, Output string
-			GoogleCN                      bool
-		}{eg.Name, eg.Doc, code, play, out, info.GoogleCN})
+		}{
+			eg.Name, eg.Doc, code, play, out,
+		}
+		err := t.Execute(&buf, &newPage)
 		if err != nil {
 			log.Print(err)
 		}
 	}
-	return buf.String()
+	return template.HTML(buf.String())
 }
 
 // replaceLeadingIndentation replaces oldIndent at the beginning of each line
@@ -190,7 +197,7 @@
 
 // example_nameFunc takes an example function name and returns its display
 // name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)".
-func (p *Presentation) example_nameFunc(s string) string {
+func example_nameFunc(s string) string {
 	name, suffix := pkgdoc.SplitExampleName(s)
 	// replace _ with . for method names
 	name = strings.Replace(name, "_", ".", 1)
@@ -203,7 +210,7 @@
 
 // example_suffixFunc takes an example function name and returns its suffix in
 // parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)".
-func (p *Presentation) example_suffixFunc(name string) string {
+func example_suffixFunc(name string) string {
 	_, suffix := pkgdoc.SplitExampleName(name)
 	return suffix
 }
diff --git a/internal/godoc/godoc.go b/internal/godoc/godoc.go
index 33bd631..7e6f989 100644
--- a/internal/godoc/godoc.go
+++ b/internal/godoc/godoc.go
@@ -13,67 +13,35 @@
 	"go/ast"
 	"go/doc"
 	"go/token"
+	"html"
+	"html/template"
 	"path"
-	"strconv"
 	"strings"
-	"text/template"
 
 	"golang.org/x/website/internal/history"
 	"golang.org/x/website/internal/pkgdoc"
 )
 
 func (p *Presentation) initFuncMap() {
-	// Template function maps.
-	// Convention: template function names ending in "_html" or "_url" produce
-	//             HTML- or URL-escaped strings; all other function results may
-	//             require explicit escaping in the template.
-
-	p.DocFuncs = template.FuncMap{
+	p.docFuncs = template.FuncMap{
 		"code":     p.code,
 		"releases": func() []*history.Major { return history.Majors },
 	}
-
-	p.SiteFuncs = template.FuncMap{
-		// various helpers
-		"filename": filenameFunc,
-		"since":    p.api.Func,
-
-		// formatting of AST nodes
-		"node":         p.nodeFunc,
-		"node_html":    p.node_htmlFunc,
-		"comment_html": comment_htmlFunc,
-		"sanitize":     sanitizeFunc,
-
-		// support for URL attributes
-		"pkgLink":       pkgLinkFunc,
-		"srcLink":       srcLinkFunc,
-		"posLink_url":   posLink_urlFunc,
-		"docLink":       docLinkFunc,
-		"queryLink":     queryLinkFunc,
-		"srcBreadcrumb": srcBreadcrumbFunc,
-		"srcToPkgLink":  srcToPkgLinkFunc,
-
-		// formatting of Examples
-		"example_html":   p.example_htmlFunc,
-		"example_name":   p.example_nameFunc,
-		"example_suffix": p.example_suffixFunc,
-
-		// Number operation
-		"multiply": multiply,
-
-		// formatting of PageInfoMode query string
-		"modeQueryString": modeQueryString,
-	}
 }
 
-func multiply(a, b int) int { return a * b }
+var siteFuncs = template.FuncMap{
+	// various helpers
+	"basename": path.Base,
 
-func filenameFunc(name string) string {
-	_, localname := path.Split(name)
-	return localname
+	// formatting of Examples
+	"example_name":   example_nameFunc,
+	"example_suffix": example_suffixFunc,
+
+	// Number operation
+	"multiply": func(a, b int) int { return a * b },
 }
 
-func pkgLinkFunc(path string) string {
+func srcToPkg(path string) string {
 	// because of the irregular mapping under goroot
 	// we need to correct certain relative paths
 	path = strings.TrimPrefix(path, "/")
@@ -82,26 +50,26 @@
 	return "pkg/" + path
 }
 
-// srcToPkgLinkFunc builds an <a> tag linking to the package
-// documentation of relpath.
-func srcToPkgLinkFunc(relpath string) string {
-	relpath = pkgLinkFunc(relpath)
-	relpath = path.Dir(relpath)
-	if relpath == "pkg" {
+// SrcPkgLink builds an <a> tag linking to the package documentation
+// for p.SrcPath.
+func (p *Page) SrcPkgLink() template.HTML {
+	dir := path.Dir(srcToPkg(p.SrcPath))
+	if dir == "pkg" {
 		return `<a href="/pkg">Index</a>`
 	}
-	return fmt.Sprintf(`<a href="/%s">%s</a>`, relpath, relpath[len("pkg/"):])
+	dir = html.EscapeString(dir)
+	return template.HTML(fmt.Sprintf(`<a href="/%s">%s</a>`, dir, dir[len("pkg/"):]))
 }
 
-// srcBreadcrumbFun converts each segment of relpath to a HTML <a>.
+// SrcBreadcrumb converts each segment of p.SrcPath to a HTML <a>.
 // Each segment links to its corresponding src directories.
-func srcBreadcrumbFunc(relpath string) string {
-	segments := strings.Split(relpath, "/")
+func (p *Page) SrcBreadcrumb() template.HTML {
+	segments := strings.Split(p.SrcPath, "/")
 	var buf bytes.Buffer
 	var selectedSegment string
 	var selectedIndex int
 
-	if strings.HasSuffix(relpath, "/") {
+	if strings.HasSuffix(p.SrcPath, "/") {
 		// relpath is a directory ending with a "/".
 		// Selected segment is the segment before the last slash.
 		selectedIndex = len(segments) - 2
@@ -113,18 +81,22 @@
 
 	for i := range segments[:selectedIndex] {
 		buf.WriteString(fmt.Sprintf(`<a href="/%s">%s</a>/`,
-			strings.Join(segments[:i+1], "/"),
-			segments[i],
+			html.EscapeString(strings.Join(segments[:i+1], "/")),
+			html.EscapeString(segments[i]),
 		))
 	}
 
 	buf.WriteString(`<span class="text-muted">`)
-	buf.WriteString(selectedSegment)
+	buf.WriteString(html.EscapeString(selectedSegment))
 	buf.WriteString(`</span>`)
-	return buf.String()
+	return template.HTML(buf.String())
 }
 
-func posLink_urlFunc(info *pkgdoc.Page, n interface{}) string {
+// SrcPosLink returns a link to the specific source code position containing n,
+// which must be either an ast.Node or a *doc.Note.
+// The current package is deduced from p.Data, which must be a *pkgdoc.Page.
+func (p *Page) SrcPosLink(n interface{}) template.HTML {
+	info := p.Data.(*pkgdoc.Page)
 	// n must be an ast.Node or a *doc.Note
 	var pos, end token.Pos
 
@@ -136,7 +108,7 @@
 		pos = n.Pos
 		end = n.End
 	default:
-		panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n))
+		panic(fmt.Sprintf("wrong type for SrcPosLink template formatter: %T", n))
 	}
 
 	var relpath string
@@ -156,8 +128,11 @@
 	return srcPosLinkFunc(relpath, line, low, high)
 }
 
-func srcPosLinkFunc(s string, line, low, high int) string {
-	s = srcLinkFunc(s)
+func srcPosLinkFunc(s string, line, low, high int) template.HTML {
+	s = path.Clean("/" + s)
+	if !strings.HasPrefix(s, "/src/") {
+		s = "/src" + s
+	}
 	var buf bytes.Buffer
 	template.HTMLEscape(&buf, []byte(s))
 	// selection ranges are of form "s=low:high"
@@ -175,30 +150,5 @@
 	if line > 0 {
 		fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping
 	}
-	return buf.String()
-}
-
-func srcLinkFunc(s string) string {
-	s = path.Clean("/" + s)
-	if !strings.HasPrefix(s, "/src/") {
-		s = "/src" + s
-	}
-	return s
-}
-
-// queryLinkFunc returns a URL for a line in a source file with a highlighted
-// query term.
-// s is expected to be a path to a source file.
-// query is expected to be a string that has already been appropriately escaped
-// for use in a URL query.
-func queryLinkFunc(s, query string, line int) string {
-	url := path.Clean("/"+s) + "?h=" + query
-	if line > 0 {
-		url += "#L" + strconv.Itoa(line)
-	}
-	return url
-}
-
-func docLinkFunc(s string, ident string) string {
-	return path.Clean("/pkg/"+s) + "/#" + ident
+	return template.HTML(buf.String())
 }
diff --git a/internal/godoc/godoc_test.go b/internal/godoc/godoc_test.go
index 74e472c..eba1bd0 100644
--- a/internal/godoc/godoc_test.go
+++ b/internal/godoc/godoc_test.go
@@ -12,13 +12,14 @@
 	"fmt"
 	"go/parser"
 	"go/token"
+	"html/template"
 	"strings"
 	"testing"
 
 	"golang.org/x/website/internal/pkgdoc"
 )
 
-func TestPkgLinkFunc(t *testing.T) {
+func TestSrcToPkg(t *testing.T) {
 	for _, tc := range []struct {
 		path string
 		want string
@@ -27,9 +28,11 @@
 		{"src/fmt", "pkg/fmt"},
 		{"/fmt", "pkg/fmt"},
 		{"fmt", "pkg/fmt"},
+		{"src/pkg/fmt", "pkg/fmt"},
+		{"/src/pkg/fmt", "pkg/fmt"},
 	} {
-		if got := pkgLinkFunc(tc.path); got != tc.want {
-			t.Errorf("pkgLinkFunc(%v) = %v; want %v", tc.path, got, tc.want)
+		if got := srcToPkg(tc.path); got != tc.want {
+			t.Errorf("srcToPkg(%v) = %v; want %v", tc.path, got, tc.want)
 		}
 	}
 }
@@ -40,7 +43,7 @@
 		line int
 		low  int
 		high int
-		want string
+		want template.HTML
 	}{
 		{"/src/fmt/print.go", 42, 30, 50, "/src/fmt/print.go?s=30:50#L32"},
 		{"/src/fmt/print.go", 2, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
@@ -51,64 +54,15 @@
 		{"fmt/print.go", 0, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
 	} {
 		if got := srcPosLinkFunc(tc.src, tc.line, tc.low, tc.high); got != tc.want {
-			t.Errorf("srcLinkFunc(%v, %v, %v, %v) = %v; want %v", tc.src, tc.line, tc.low, tc.high, got, tc.want)
+			t.Errorf("srcPosLink(%v, %v, %v, %v) = %v; want %v", tc.src, tc.line, tc.low, tc.high, got, tc.want)
 		}
 	}
 }
 
-func TestSrcLinkFunc(t *testing.T) {
+func TestSanitize(t *testing.T) {
 	for _, tc := range []struct {
-		src  string
-		want string
-	}{
-		{"/src/fmt/print.go", "/src/fmt/print.go"},
-		{"src/fmt/print.go", "/src/fmt/print.go"},
-		{"/fmt/print.go", "/src/fmt/print.go"},
-		{"fmt/print.go", "/src/fmt/print.go"},
-	} {
-		if got := srcLinkFunc(tc.src); got != tc.want {
-			t.Errorf("srcLinkFunc(%v) = %v; want %v", tc.src, got, tc.want)
-		}
-	}
-}
-
-func TestQueryLinkFunc(t *testing.T) {
-	for _, tc := range []struct {
-		src   string
-		query string
-		line  int
-		want  string
-	}{
-		{"/src/fmt/print.go", "Sprintf", 33, "/src/fmt/print.go?h=Sprintf#L33"},
-		{"/src/fmt/print.go", "Sprintf", 0, "/src/fmt/print.go?h=Sprintf"},
-		{"src/fmt/print.go", "EOF", 33, "/src/fmt/print.go?h=EOF#L33"},
-		{"src/fmt/print.go", "a%3f+%26b", 1, "/src/fmt/print.go?h=a%3f+%26b#L1"},
-	} {
-		if got := queryLinkFunc(tc.src, tc.query, tc.line); got != tc.want {
-			t.Errorf("queryLinkFunc(%v, %v, %v) = %v; want %v", tc.src, tc.query, tc.line, got, tc.want)
-		}
-	}
-}
-
-func TestDocLinkFunc(t *testing.T) {
-	for _, tc := range []struct {
-		src   string
-		ident string
-		want  string
-	}{
-		{"fmt", "Sprintf", "/pkg/fmt/#Sprintf"},
-		{"fmt", "EOF", "/pkg/fmt/#EOF"},
-	} {
-		if got := docLinkFunc(tc.src, tc.ident); got != tc.want {
-			t.Errorf("docLinkFunc(%v, %v) = %v; want %v", tc.src, tc.ident, got, tc.want)
-		}
-	}
-}
-
-func TestSanitizeFunc(t *testing.T) {
-	for _, tc := range []struct {
-		src  string
-		want string
+		src  template.HTML
+		want template.HTML
 	}{
 		{},
 		{"foo", "foo"},
@@ -121,8 +75,8 @@
 		{"{   a,   b}", "{a, b}"},
 		{"[   a,   b]", "[a, b]"},
 	} {
-		if got := sanitizeFunc(tc.src); got != tc.want {
-			t.Errorf("sanitizeFunc(%v) = %v; want %v", tc.src, got, tc.want)
+		if got := sanitize(tc.src); got != tc.want {
+			t.Errorf("sanitize(%v) = %v; want %v", tc.src, got, tc.want)
 		}
 	}
 }
@@ -241,11 +195,15 @@
 	pi := &pkgdoc.Page{
 		FSet: fset,
 	}
+	pg := &Page{
+		pres: p,
+		Data: pi,
+	}
 	sep := ""
 	for _, decl := range af.Decls {
 		buf.WriteString(sep)
 		sep = "\n"
-		buf.WriteString(p.node_htmlFunc(pi, decl, true))
+		buf.WriteString(string(pg.Node(decl)))
 	}
 	return buf.String()
 }
@@ -279,29 +237,29 @@
 func TestSrcBreadcrumbFunc(t *testing.T) {
 	for _, tc := range []struct {
 		path string
-		want string
+		want template.HTML
 	}{
 		{"src/", `<span class="text-muted">src/</span>`},
 		{"src/fmt/", `<a href="/src">src</a>/<span class="text-muted">fmt/</span>`},
 		{"src/fmt/print.go", `<a href="/src">src</a>/<a href="/src/fmt">fmt</a>/<span class="text-muted">print.go</span>`},
 	} {
-		if got := srcBreadcrumbFunc(tc.path); got != tc.want {
+		if got := (&Page{SrcPath: tc.path}).SrcBreadcrumb(); got != tc.want {
 			t.Errorf("srcBreadcrumbFunc(%v) = %v; want %v", tc.path, got, tc.want)
 		}
 	}
 }
 
-func TestSrcToPkgLinkFunc(t *testing.T) {
+func TestSrcPkgLink(t *testing.T) {
 	for _, tc := range []struct {
 		path string
-		want string
+		want template.HTML
 	}{
 		{"src/", `<a href="/pkg">Index</a>`},
 		{"src/fmt/", `<a href="/pkg/fmt">fmt</a>`},
 		{"pkg/", `<a href="/pkg">Index</a>`},
 		{"pkg/LICENSE", `<a href="/pkg">Index</a>`},
 	} {
-		if got := srcToPkgLinkFunc(tc.path); got != tc.want {
+		if got := (&Page{SrcPath: tc.path}).SrcPkgLink(); got != tc.want {
 			t.Errorf("srcToPkgLinkFunc(%v) = %v; want %v", tc.path, got, tc.want)
 		}
 	}
diff --git a/internal/godoc/meta.go b/internal/godoc/meta.go
index 0c5809e..27f4def 100644
--- a/internal/godoc/meta.go
+++ b/internal/godoc/meta.go
@@ -17,7 +17,6 @@
 )
 
 var (
-	doctype   = []byte("<!DOCTYPE ")
 	jsonStart = []byte("<!--{")
 	jsonEnd   = []byte("}-->")
 )
diff --git a/internal/godoc/page.go b/internal/godoc/page.go
index 6c06ba5..9426dd7 100644
--- a/internal/godoc/page.go
+++ b/internal/godoc/page.go
@@ -9,50 +9,64 @@
 
 import (
 	"net/http"
-	"os"
-	"path/filepath"
 	"runtime"
 )
 
-// Page describes the contents of the top-level godoc webpage.
+// A Page describes the contents of a webpage to be served.
+//
+// A Page's Methods are for use by the templates rendering the page.
 type Page struct {
-	Title    string
-	Tabtitle string
-	Subtitle string
-	SrcPath  string
-	Query    string
-	Body     []byte
-	GoogleCN bool // page is being served from golang.google.cn
+	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
 
-	// filled in by ServePage
-	Version         string
-	GoogleAnalytics string
+	// 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
+
+	// Filled in automatically by ServePage
+	GoogleCN        bool   // page is being served from golang.google.cn
+	GoogleAnalytics string // Google Analytics tag
+	Version         string // current Go version
+
+	pres *Presentation
 }
 
-func (p *Presentation) ServePage(w http.ResponseWriter, page Page) {
-	if page.Tabtitle == "" {
-		page.Tabtitle = page.Title
+// fullPage returns a copy of page with the “automatic” fields filled in.
+func (p *Presentation) fullPage(r *http.Request, page Page) Page {
+	if page.TabTitle == "" {
+		page.TabTitle = page.Title
 	}
 	page.Version = runtime.Version()
+	page.GoogleCN = p.googleCN(r)
 	page.GoogleAnalytics = p.GoogleAnalytics
-	applyTemplateToResponseWriter(w, p.GodocHTML, page)
+	page.pres = p
+	return page
 }
 
-func (p *Presentation) ServeError(w http.ResponseWriter, r *http.Request, relpath string, err error) {
+// ServePage responds to the request with the content described by page.
+func (p *Presentation) ServePage(w http.ResponseWriter, r *http.Request, page Page) {
+	page = p.fullPage(r, page)
+	applyTemplateToResponseWriter(w, p.Templates.Lookup("site.html"), &page)
+}
+
+// ServeError responds to the request with the given error.
+func (p *Presentation) ServeError(w http.ResponseWriter, r *http.Request, err error) {
 	w.WriteHeader(http.StatusNotFound)
-	if perr, ok := err.(*os.PathError); ok {
-		rel, err := filepath.Rel(runtime.GOROOT(), perr.Path)
-		if err != nil {
-			perr.Path = "REDACTED"
-		} else {
-			perr.Path = filepath.Join("$GOROOT", rel)
-		}
-	}
-	p.ServePage(w, Page{
-		Title:           "File " + relpath,
-		Subtitle:        relpath,
-		Body:            applyTemplate(p.ErrorHTML, "errorHTML", err),
-		GoogleCN:        p.googleCN(r),
-		GoogleAnalytics: p.GoogleAnalytics,
+	p.ServePage(w, r, Page{
+		Title:    r.URL.Path,
+		Template: "error.html",
+		Data:     err,
 	})
 }
diff --git a/internal/godoc/pres.go b/internal/godoc/pres.go
index b60e9b6..51928c4 100644
--- a/internal/godoc/pres.go
+++ b/internal/godoc/pres.go
@@ -8,15 +8,15 @@
 package godoc
 
 import (
+	"html/template"
 	"io/fs"
 	"net/http"
-	"text/template"
 
 	"golang.org/x/website/internal/api"
 	"golang.org/x/website/internal/pkgdoc"
 )
 
-// Presentation generates output from a file system.
+// Presentation is a website served from a file system.
 type Presentation struct {
 	fs  fs.FS
 	api api.DB
@@ -24,12 +24,7 @@
 	mux        *http.ServeMux
 	fileServer http.Handler
 
-	DirlistHTML,
-	ErrorHTML,
-	ExampleHTML,
-	GodocHTML,
-	PackageHTML,
-	PackageRootHTML *template.Template
+	Templates *template.Template
 
 	// GoogleCN reports whether this request should be marked GoogleCN.
 	// If the function is nil, no requests are marked GoogleCN.
@@ -39,8 +34,7 @@
 	// tracking ID to each page.
 	GoogleAnalytics string
 
-	DocFuncs  template.FuncMap
-	SiteFuncs template.FuncMap
+	docFuncs template.FuncMap
 }
 
 // NewPresentation returns a new Presentation from a file system.
@@ -61,31 +55,19 @@
 	}
 	p.mux.Handle("/cmd/", docs)
 	p.mux.Handle("/pkg/", docs)
-	p.mux.HandleFunc("/", p.ServeFile)
+	p.mux.HandleFunc("/", p.serveFile)
 	p.initFuncMap()
 
-	if p.DirlistHTML, err = p.ReadTemplate("dirlist.html"); err != nil {
+	t, err := template.New("").Funcs(siteFuncs).ParseFS(fsys, "lib/godoc/*.html")
+	if err != nil {
 		return nil, err
 	}
-	if p.ErrorHTML, err = p.ReadTemplate("error.html"); err != nil {
-		return nil, err
-	}
-	if p.ExampleHTML, err = p.ReadTemplate("example.html"); err != nil {
-		return nil, err
-	}
-	if p.GodocHTML, err = p.ReadTemplate("godoc.html"); err != nil {
-		return nil, err
-	}
-	if p.PackageHTML, err = p.ReadTemplate("package.html"); err != nil {
-		return nil, err
-	}
-	if p.PackageRootHTML, err = p.ReadTemplate("packageroot.html"); err != nil {
-		return nil, err
-	}
+	p.Templates = t
 
 	return p, nil
 }
 
+// ServeHTTP implements http.Handler, dispatching the request appropriately.
 func (p *Presentation) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	p.mux.ServeHTTP(w, r)
 }
@@ -93,15 +75,3 @@
 func (p *Presentation) googleCN(r *http.Request) bool {
 	return p.GoogleCN != nil && p.GoogleCN(r)
 }
-
-func (p *Presentation) ReadTemplate(name string) (*template.Template, error) {
-	data, err := fs.ReadFile(p.fs, "lib/godoc/"+name)
-	if err != nil {
-		return nil, err
-	}
-	t, err := template.New(name).Funcs(p.SiteFuncs).Parse(string(data))
-	if err != nil {
-		return nil, err
-	}
-	return t, nil
-}
diff --git a/internal/godoc/server.go b/internal/godoc/server.go
index c10b189..90b119f 100644
--- a/internal/godoc/server.go
+++ b/internal/godoc/server.go
@@ -12,6 +12,7 @@
 	"encoding/json"
 	"fmt"
 	htmlpkg "html"
+	"html/template"
 	"io"
 	"io/fs"
 	"log"
@@ -20,7 +21,6 @@
 	"regexp"
 	"strconv"
 	"strings"
-	"text/template"
 
 	"golang.org/x/website/internal/pkgdoc"
 	"golang.org/x/website/internal/spec"
@@ -47,7 +47,8 @@
 	}
 
 	// TODO(rsc): URL should be clean already.
-	relpath := path.Clean(strings.TrimPrefix(r.URL.Path, "/pkg/"))
+	relpath := path.Clean(strings.TrimPrefix(r.URL.Path, "/pkg"))
+	relpath = strings.TrimPrefix(relpath, "/")
 
 	abspath := path.Join("/src", relpath)
 	mode := pkgdoc.ParseMode(r.FormValue("m"))
@@ -60,7 +61,7 @@
 	info := pkgdoc.Doc(h.d, abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
 	if info.Err != nil {
 		log.Print(info.Err)
-		h.p.ServeError(w, r, relpath, info.Err)
+		h.p.ServeError(w, r, info.Err)
 		return
 	}
 
@@ -93,23 +94,23 @@
 		tabtitle = "Commands"
 	}
 
-	info.GoogleCN = h.p.googleCN(r)
-	var body []byte
+	name := "package.html"
 	if info.Dirname == "/src" {
-		body = applyTemplate(h.p.PackageRootHTML, "packageRootHTML", info)
-	} else {
-		body = applyTemplate(h.p.PackageHTML, "packageHTML", info)
+		name = "packageroot.html"
 	}
-	h.p.ServePage(w, Page{
+	h.p.ServePage(w, r, Page{
 		Title:    title,
-		Tabtitle: tabtitle,
+		TabTitle: tabtitle,
 		Subtitle: subtitle,
-		Body:     body,
-		GoogleCN: info.GoogleCN,
+		Template: name,
+		Data:     info,
 	})
 }
 
-func modeQueryString(m pkgdoc.Mode) string {
+// ModeQuery returns the "?m=..." query for the current page.
+// The page's Data must be a *pkgdoc.Page (to find the mode).
+func (p *Page) ModeQuery() string {
+	m := p.Data.(*pkgdoc.Page).Mode
 	s := m.String()
 	if s == "" {
 		return ""
@@ -117,12 +118,17 @@
 	return "?m=" + s
 }
 
-func applyTemplate(t *template.Template, name string, data interface{}) []byte {
+// Invoke invokes the template with the given name on
+// a copy of p with .Data set to data, returning the resulting HTML.
+func (p *Page) Invoke(name string, data interface{}) template.HTML {
+	t := p.pres.Templates.Lookup(name)
 	var buf bytes.Buffer
-	if err := t.Execute(&buf, data); err != nil {
-		log.Printf("%s.Execute: %s", name, err)
+	p1 := *p
+	p1.Data = data
+	if err := t.Execute(&buf, &p1); err != nil {
+		log.Printf("%s.Execute: %s", t.Name(), err)
 	}
-	return buf.Bytes()
+	return template.HTML(buf.String())
 }
 
 type writerCapturesErr struct {
@@ -202,12 +208,12 @@
 	src, err := fs.ReadFile(p.fs, toFS(abspath))
 	if err != nil {
 		log.Printf("ReadFile: %s", err)
-		p.ServeError(w, r, relpath, err)
+		p.ServeError(w, r, err)
 		return
 	}
 
 	if r.FormValue("m") == "text" {
-		p.ServeText(w, src)
+		p.serveText(w, src)
 		return
 	}
 
@@ -229,12 +235,11 @@
 	if strings.HasSuffix(relpath, ".go") {
 		title = "Source file"
 	}
-	p.ServePage(w, Page{
+	p.ServePage(w, r, Page{
 		Title:    title,
 		SrcPath:  relpath,
-		Tabtitle: relpath,
-		Body:     buf.Bytes(),
-		GoogleCN: p.googleCN(r),
+		TabTitle: relpath,
+		Data:     template.HTML(buf.String()),
 	})
 }
 
@@ -245,7 +250,7 @@
 
 	list, err := fs.ReadDir(p.fs, toFS(abspath))
 	if err != nil {
-		p.ServeError(w, r, relpath, err)
+		p.ServeError(w, r, err)
 		return
 	}
 
@@ -257,15 +262,17 @@
 		}
 	}
 
-	p.ServePage(w, Page{
+	p.ServePage(w, r, Page{
 		Title:    "Directory",
 		SrcPath:  relpath,
-		Tabtitle: relpath,
-		Body:     applyTemplate(p.DirlistHTML, "dirlistHTML", info),
-		GoogleCN: p.googleCN(r),
+		TabTitle: relpath,
+		Template: "dirlist.html",
+		Data:     info,
 	})
 }
 
+var doctype = []byte("<!DOCTYPE ")
+
 func (p *Presentation) serveHTML(w http.ResponseWriter, r *http.Request, f *file) {
 	src := f.Body
 	isMarkdown := strings.HasSuffix(f.FilePath, ".md")
@@ -280,21 +287,21 @@
 	page := Page{
 		Title:    f.Title,
 		Subtitle: f.Subtitle,
-		GoogleCN: p.googleCN(r),
 	}
 
 	// evaluate as template if indicated
 	if f.Template {
-		tmpl, err := template.New("main").Funcs(p.DocFuncs).Parse(string(src))
+		page = p.fullPage(r, page)
+		tmpl, err := template.New("main").Funcs(p.docFuncs).Parse(string(src))
 		if err != nil {
 			log.Printf("parsing template %s: %v", f.Path, err)
-			p.ServeError(w, r, f.Path, err)
+			p.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)
-			p.ServeError(w, r, f.Path, err)
+			p.ServeError(w, r, err)
 			return
 		}
 		src = buf.Bytes()
@@ -306,7 +313,7 @@
 		html, err := renderMarkdown(src)
 		if err != nil {
 			log.Printf("executing markdown %s: %v", f.Path, err)
-			p.ServeError(w, r, f.Path, err)
+			p.ServeError(w, r, err)
 			return
 		}
 		src = html
@@ -319,12 +326,8 @@
 		src = buf.Bytes()
 	}
 
-	page.Body = src
-	p.ServePage(w, page)
-}
-
-func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
-	p.serveFile(w, r)
+	page.Data = template.HTML(src)
+	p.ServePage(w, r, page)
 }
 
 func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
@@ -361,7 +364,7 @@
 				return
 			}
 		}
-		p.ServeError(w, r, relpath, err)
+		p.ServeError(w, r, err)
 		return
 	}
 
@@ -385,7 +388,7 @@
 	p.fileServer.ServeHTTP(w, r)
 }
 
-func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
+func (p *Presentation) serveText(w http.ResponseWriter, text []byte) {
 	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 	w.Write(text)
 }
diff --git a/internal/godoc/server_test.go b/internal/godoc/server_test.go
index 152e934..1829036 100644
--- a/internal/godoc/server_test.go
+++ b/internal/godoc/server_test.go
@@ -14,14 +14,13 @@
 	"strings"
 	"testing"
 	"testing/fstest"
-	"text/template"
 )
 
 func testServeBody(t *testing.T, p *Presentation, path, body string) {
 	t.Helper()
 	r := &http.Request{URL: &url.URL{Path: path}}
 	rw := httptest.NewRecorder()
-	p.ServeFile(rw, r)
+	p.serveFile(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)
@@ -30,11 +29,12 @@
 
 func TestRedirectAndMetadata(t *testing.T) {
 	fsys := fstest.MapFS{
-		"doc/x/index.html": {Data: []byte("Hello, x.")},
+		"doc/x/index.html":    {Data: []byte("Hello, x.")},
+		"lib/godoc/site.html": {Data: []byte(`{{.Data}}`)},
 	}
-	p := &Presentation{
-		fs:        fsys,
-		GodocHTML: template.Must(template.New("").Parse(`{{printf "%s" .Body}}`)),
+	p, err := NewPresentation(fsys)
+	if err != nil {
+		t.Fatal(err)
 	}
 
 	// Test that redirect is sent back correctly.
@@ -43,7 +43,7 @@
 
 	r := &http.Request{URL: &url.URL{Path: dir + "index.html"}}
 	rw := httptest.NewRecorder()
-	p.ServeFile(rw, r)
+	p.serveFile(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)
@@ -53,12 +53,13 @@
 }
 
 func TestMarkdown(t *testing.T) {
-	p := &Presentation{
-		fs: fstest.MapFS{
-			"doc/test.md":  {Data: []byte("**bold**")},
-			"doc/test2.md": {Data: []byte(`{{"*template*"}}`)},
-		},
-		GodocHTML: template.Must(template.New("").Parse(`{{printf "%s" .Body}}`)),
+	p, err := NewPresentation(fstest.MapFS{
+		"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>")
diff --git a/internal/godoc/template.go b/internal/godoc/template.go
index 138b71f..214a159 100644
--- a/internal/godoc/template.go
+++ b/internal/godoc/template.go
@@ -37,6 +37,7 @@
 import (
 	"bytes"
 	"fmt"
+	"html/template"
 	"io/fs"
 	"log"
 	"regexp"
@@ -74,7 +75,7 @@
 	return ""
 }
 
-func (p *Presentation) code(file string, arg ...interface{}) (s string, err error) {
+func (p *Presentation) code(file string, arg ...interface{}) (_ template.HTML, err error) {
 	defer func() {
 		if r := recover(); r != nil {
 			err = fmt.Errorf("%v", r)
@@ -105,7 +106,7 @@
 	buf.Write(texthtml.Format([]byte(text), texthtml.Config{GoComments: true}))
 	// Include the command as a comment.
 	text = fmt.Sprintf("<pre><!--{{%s}}\n-->%s</pre>", command, buf.Bytes())
-	return text, nil
+	return template.HTML(text), nil
 }
 
 // parseArg returns the integer or string value of the argument and tells which it is.
diff --git a/internal/pkgdoc/doc.go b/internal/pkgdoc/doc.go
index a5b98ac..02e7b5b 100644
--- a/internal/pkgdoc/doc.go
+++ b/internal/pkgdoc/doc.go
@@ -44,9 +44,8 @@
 }
 
 type Page struct {
-	Dirname  string // directory containing the package
-	Err      error  // error or nil
-	GoogleCN bool   // page is being served from golang.google.cn
+	Dirname string // directory containing the package
+	Err     error  // error or nil
 
 	Mode Mode // display metadata from query string