[x/go.dev] all: simplify page↔template API

Overall, this CL changes Pages from an object with behaviors
to just plain old data operated on by templates. All the necessary
behaviors, like loading information about related pages, become
registered functions.

This CL simplifies the mental model: a page is literally nothing more
than data rendered by a template.

For example, instead of Parent and CurrentSection returning *Page
objects, they now return just the path (/foo) of the page, which can
be passed to the "page" function to load that page if needed.
Section now returns that path form too, instead of omitting the
leading slash. So "page .Section" is the section's landing page.
CurrentSection is actually gone entirely, but if needed it would be
"page .Dir".

This CL merges the Page.Params and other fields into a single map,
so that templates don't have to say .Params all over the place just to
get at per-page in what is already the per-page data object.
In that map, the convention is that uppercase keys are the ones
supplied by the site package itself, and the lowercase keys are
the ones from the  page's YAML data. But that's just a convention.
".Params.foo" is now just ".foo".

A few YAML fields known to Hugo seem to have been available in
multiple casings: in particular, series and Series, title and Title.
Fix the templates to use the form from the YAML: lowercase.

Similarly, replace where and sort, which required a custom
expression evaluator separate from the template engine, with
more limited functions "pages" (which loads pages from a given
set of files) and "newest" (which sorts them by date and then title).
These are less to understand, and the rest can be done with
plain templates (with the help of a new "add" function).

Unexport Page since nothing needs it to be exported.

Change-Id: I8a927515c45880f52dcc3cea38ac3252fe1bff15
X-GoDev-Commit: 7b0e82257a7b492fae378ea9ee51cf7c924d98a1
diff --git a/go.dev/_content/index.md b/go.dev/_content/index.md
index 6426448..2223af5 100644
--- a/go.dev/_content/index.md
+++ b/go.dev/_content/index.md
@@ -68,7 +68,6 @@
   </div>
 </section>
 <section class="WhoUses">
-  {{$solutions := index (where .Pages "Section" "solutions") 0}}
   <div class="WhoUses-gridContainer">
     <div class="WhoUses-header">
       <h2 class="WhoUses-headerH2">Companies using Go</h2>
@@ -80,17 +79,17 @@
     </div>
   <div class="WhoUsesCaseStudyList">
     <ul class="WhoUsesCaseStudyList-gridContainer">
-    {{range where $solutions.Pages "Params.series" "Case Studies"}}
-      {{if .Params.link }}
-        {{if .Params.inLandingPageGrid }}
+    {{range newest (pages "solutions/*")}}{{if eq .series "Case Studies"}}
+      {{if .link }}
+        {{if .inLandingPageGrid }}
           <li class="WhoUsesCaseStudyList-caseStudy">
-            <a href="{{.Params.link}}" target="_blank" rel="noopener"
+            <a href="{{.link}}" target="_blank" rel="noopener"
               class="WhoUsesCaseStudyList-caseStudyLink">
               <img
                 loading="lazy"
                 height="48"
                 width="30%"
-                src="/images/logos/{{.Params.logoSrc}}"
+                src="/images/logos/{{.logoSrc}}"
                 class="WhoUsesCaseStudyList-logo"
                 alt="">
             </a>
@@ -103,14 +102,14 @@
               loading="lazy"
               height="48"
               width="30%"
-              src="/images/logos/{{.Params.logoSrc}}"
+              src="/images/logos/{{.logoSrc}}"
               class="WhoUsesCaseStudyList-logo"
               alt="">
             <p>View case study</p>
           </a>
         </li>
       {{end}}
-    {{end}}
+    {{end}}{{end}}
     </ul>
   </div>
 </section>
diff --git a/go.dev/_content/solutions/index.md b/go.dev/_content/solutions/index.md
index 4767899..8d15e21 100644
--- a/go.dev/_content/solutions/index.md
+++ b/go.dev/_content/solutions/index.md
@@ -2,36 +2,43 @@
 title: Why Go
 ---
 
+{{$solutions := pages "solutions/*"}}
 <section class="Solutions-headline">
   <div class="GoCarousel" id="SolutionsHeroCarousel-carousel">
     <div class="GoCarousel-controlsContainer">
       <div class="GoCarousel-wrapper SolutionsHeroCarousel-wrapper">
         <ul class="js-solutionsHeroCarouselSlides SolutionsHeroCarousel-slides">
-          {{range where .Pages "Params.series" "Case Studies" | first 3}}
-          <li class="SolutionsHeroCarousel-slide">
-            <div class="Solutions-headlineImg">
-              <img
-                src="/images/{{.Params.carouselImgSrc}}"
-                alt="{{.LinkTitle}}"
-              />
-            </div>
-            <div class="Solutions-headlineText">
-              <p class="Solutions-headlineNotification">RECENTLY UPDATED</p>
-              <h2>
-                {{.LinkTitle}}
-              </h2>
-              <p class="Solutions-headlineBody">
-                {{with .Params.quote}}{{.}}{{end}}
-                <a href="{{.Path}}"
-                  >Learn more
-                  <i class="material-icons Solutions-forwardArrowIcon"
-                    >arrow_forward</i
-                  >
-                </a>
-              </p>
-            </div>
-          </li>
-          {{end}}
+          {{- $n := 0}}
+          {{- range newest $solutions}}
+            {{- if eq .series "Case Studies"}}
+              {{- $n = add $n 1}}
+              {{- if le $n 3}}
+              <li class="SolutionsHeroCarousel-slide">
+                <div class="Solutions-headlineImg">
+                  <img
+                    src="/images/{{.carouselImgSrc}}"
+                    alt="{{.linkTitle}}"
+                  />
+                </div>
+                <div class="Solutions-headlineText">
+                  <p class="Solutions-headlineNotification">RECENTLY UPDATED</p>
+                  <h2>
+                    {{.linkTitle}}
+                  </h2>
+                  <p class="Solutions-headlineBody">
+                    {{with .quote}}{{.}}{{end}}
+                    <a href="{{.Path}}"
+                      >Learn more
+                      <i class="material-icons Solutions-forwardArrowIcon"
+                        >arrow_forward</i
+                      >
+                    </a>
+                  </p>
+                </div>
+              </li>
+              {{- end}}
+            {{- end}}
+          {{- end}}
         </ul>
       </div>
       <button
@@ -79,29 +86,28 @@
       role="tabpanel"
       tabindex="0"
     >
-      {{$solutions := where .Pages "Params.series" "Case Studies"}}
-      {{range sort $solutions "Params.company" "asc"}}
+      {{range $solutions}}{{if eq .series "Case Studies"}}
       <li class="Solutions-card">
-        {{if .Params.link}}
+        {{if .link}}
         <a
-          href="{{.Params.link}}"
+          href="{{.link}}"
           target="_blank"
           rel="noopener"
           class="Solutions-useCaseLink"
         >
           <div
-            class="Solutions-useCaseLogo Solutions-useCaseLogo--{{.Params.company}}"
+            class="Solutions-useCaseLogo Solutions-useCaseLogo--{{.company}}"
           >
             <img
               loading="lazy"
-              alt="{{.Params.company}}"
-              src="/images/logos/{{.Params.logoSrc}}"
+              alt="{{.company}}"
+              src="/images/logos/{{.logoSrc}}"
             />
           </div>
           <div class="Solutions-useCaseBody">
-            <h3 class="Solutions-useCaseTitle">{{.LinkTitle}}</h3>
+            <h3 class="Solutions-useCaseTitle">{{.linkTitle}}</h3>
             <p class="Solutions-useCaseDescription">
-              {{.Description}}
+              {{.description}}
             </p>
           </div>
           <p class="Solutions-useCaseAction">
@@ -114,21 +120,21 @@
           <div class="Solutions-useCaseLogo">
             <img
               loading="lazy"
-              alt="{{.Params.company}}"
-              src="/images/logos/{{.Params.logoSrc}}"
+              alt="{{.company}}"
+              src="/images/logos/{{.logoSrc}}"
             />
           </div>
           <div class="Solutions-useCaseBody">
-            <h3 class="Solutions-useCaseTitle">{{.LinkTitle}}</h3>
+            <h3 class="Solutions-useCaseTitle">{{.linkTitle}}</h3>
             <p class="Solutions-useCaseDescription">
-              {{with .Params.quote}}{{.}}{{end}}
+              {{with .quote}}{{.}}{{end}}
             </p>
           </div>
           <p class="Solutions-useCaseAction">View case study</p>
         </a>
         {{end}}
       </li>
-      {{ end }}
+      {{end}}{{end}}
     </ul>
     <ul
       class="js-solutionsList Solutions-cardList"
@@ -139,11 +145,11 @@
       tabindex="0"
       hidden
     >
-      {{range where .Pages "Params.series" "Use Cases"}}
+      {{range newest $solutions}}{{if eq .series "Use Cases"}}
       <li class="Solutions-card">
         <a href="{{.Path}}" class="Solutions-useCaseLink">
           <div class="Solutions-useCaseLogo">
-            {{$icon := .Params.icon}}{{if $icon}}
+            {{$icon := .icon}}{{if $icon}}
             <img
               loading="lazy"
               alt="{{$icon.alt}}"
@@ -152,9 +158,9 @@
             {{end}}
           </div>
           <div class="Solutions-useCaseBody">
-            <h3 class="Solutions-useCaseTitle">{{.LinkTitle}}</h3>
+            <h3 class="Solutions-useCaseTitle">{{.linkTitle}}</h3>
             <p class="Solutions-useCaseDescription">
-              {{.Description}}
+              {{.description}}
             </p>
           </div>
           <p class="Solutions-useCaseAction">
@@ -162,7 +168,7 @@
           </p>
         </a>
       </li>
-      {{end}}
+      {{end}}{{end}}
     </ul>
     <div class="Solutions-footer">
       <p>
diff --git a/go.dev/_templates/breadcrumbs.tmpl b/go.dev/_templates/breadcrumbs.tmpl
index 2e7b4e5..75ef476 100644
--- a/go.dev/_templates/breadcrumbs.tmpl
+++ b/go.dev/_templates/breadcrumbs.tmpl
@@ -1,14 +1,14 @@
 {{define "breadcrumbnav"}}
 {{- if .p1.Parent}}
-  {{- template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 )}}
+  {{- template "breadcrumbnav" (dict "p1" (page .p1.Parent) "p2" .p2 )}}
 {{- end}}
-{{- if not (eq .p1.Title "go.dev")}}
-      <li class="BreadcrumbNav-li {{if eq .p1 .p2}}active{{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}}">
-          {{- if .p1.Params.company}}
-            {{.p1.Params.company}}
+          {{- if .p1.company}}
+            {{.p1.company}}
           {{- else}}
-            {{.p1.Title}}
+            {{.p1.title}}
           {{- end}}
         </a>
       </li>
diff --git a/go.dev/_templates/layouts/article.tmpl b/go.dev/_templates/layouts/article.tmpl
index 1fa6431..6b6d7a3 100644
--- a/go.dev/_templates/layouts/article.tmpl
+++ b/go.dev/_templates/layouts/article.tmpl
@@ -1,6 +1,6 @@
 {{define "layout"}}
-  <article class="Article {{if .Section -}}Article--{{.Section}}{{end -}}">
-    <h1>{{.Title}}</h1>
+  <article class="Article {{if ne .Section "/"}}Article--{{trim .Section "/"}}{{end -}}">
+    <h1>{{.title}}</h1>
     {{.Content}}
   </article>
 {{end}}
\ No newline at end of file
diff --git a/go.dev/_templates/layouts/site.tmpl b/go.dev/_templates/layouts/site.tmpl
index 0055447..96b6af9 100644
--- a/go.dev/_templates/layouts/site.tmpl
+++ b/go.dev/_templates/layouts/site.tmpl
@@ -23,9 +23,9 @@
   })(window,document,'script','dataLayer','GTM-W8MVQXG');</script>
   <!-- End Google Tag Manager -->
 <script src="/js/site.js"></script>
-<title>{{.Title}}{{if not .IsHome}} - go.dev{{end}}</title>
-{{if .Params.link -}}
-<meta http-equiv="refresh" content="0; url={{.Params.link}}">
+<title>{{.title}}{{if .Parent}} - go.dev{{end}}</title>
+{{if .link -}}
+<meta http-equiv="refresh" content="0; url={{.link}}">
 {{end -}}
 </head>
 <body class="Site">
@@ -60,7 +60,7 @@
         <ul class="Header-menu">
           {{- $currentPage := .}}
           {{- range $menus.main}}
-          <li class="Header-menuItem {{if eq $currentPage.Title .name}} Header-menuItem--active{{end}}">
+          <li class="Header-menuItem {{if eq $currentPage.title .name}} Header-menuItem--active{{end}}">
             <a href="{{.url}}">{{.name}}</a>
           </li>
           {{- end}}
@@ -69,7 +69,7 @@
         </button>
       </div>
     </nav>
-    {{ if (eq .Params.Series "Use Cases") }}
+    {{ if (eq .series "Use Cases") }}
     <div class="UseCaseSubNav js-useCaseSubnav">
       <button class="UseCaseSubNav-menuHeader js-useCaseSubnavHeader">
         Jump to
@@ -94,7 +94,7 @@
     </div>
     <ul class="NavigationDrawer-list">
       {{- range $menus.main}}
-        <li class="NavigationDrawer-listItem {{if eq $currentPage.CurrentSection.Title .name}} NavigationDrawer-listItem--active{{end}}">
+        <li class="NavigationDrawer-listItem {{if eq .url $currentPage.Section}} NavigationDrawer-listItem--active{{end}}">
           <a href="{{.url}}">{{.name}}</a>
         </li>
       {{- end}}
diff --git a/go.dev/_templates/layouts/solution.tmpl b/go.dev/_templates/layouts/solution.tmpl
index 28b7006..580be32 100644
--- a/go.dev/_templates/layouts/solution.tmpl
+++ b/go.dev/_templates/layouts/solution.tmpl
@@ -1,48 +1,48 @@
 {{define "layout"}}
 <div>
-  <div class="WhoUsesSubPage-hero{{if (eq .Params.Series "Case Studies")}}--caseStudy{{else}}--useCase{{end}}">
-    <div class="WhoUsesSubPage-heroInner{{if (eq .Params.Series "Case Studies")}}--caseStudy{{else}}--useCase{{end}}">
-      <div class="WhoUsesSubPage-heroContent{{if (eq .Params.Series "Case Studies")}}--caseStudy{{else}}--useCase{{end}}">
-        <div class="WhoUsesSubPage-heroText{{if (eq .Params.Series "Case Studies")}}--caseStudy{{else}}--useCase{{end}}">
+  <div class="WhoUsesSubPage-hero{{if (eq .series "Case Studies")}}--caseStudy{{else}}--useCase{{end}}">
+    <div class="WhoUsesSubPage-heroInner{{if (eq .series "Case Studies")}}--caseStudy{{else}}--useCase{{end}}">
+      <div class="WhoUsesSubPage-heroContent{{if (eq .series "Case Studies")}}--caseStudy{{else}}--useCase{{end}}">
+        <div class="WhoUsesSubPage-heroText{{if (eq .series "Case Studies")}}--caseStudy{{else}}--useCase{{end}}">
           {{breadcrumbs .}}
-          <h1>{{.Title}}</h1>
-          {{range .Params.authors}}
+          <h1>{{.title}}</h1>
+          {{range .authors}}
             <div class="Article-author">{{.}}</div>
           {{end}}
-          {{if .Params.date}}
+          {{if .date}}
             <div class="Article-date">{{.Date.Format "2 January 2006"}}</div>
           {{end}}
         </div>
-          {{if .Params.company}}
+          {{if .company}}
             <div class="WhoUsesSubPage-heroImg">
-              <img src="/images/{{ .Params.heroImgSrc }}" alt="{{.Params.company }}">
+              <img src="/images/{{ .heroImgSrc }}" alt="{{.company }}">
             </div>
           {{end}}
         </div>
       </div>
 
   </div>
-  <article class="Article {{if .Section -}}Article--{{.Section}}{{end -}}">
-    {{if (eq .Params.Series "Case Studies") }}
+  <article class="Article {{if ne .Section "/"}}Article--{{trim .Section "/"}}{{end -}}">
+    {{if (eq .series "Case Studies") }}
       <div class="CaseStudy-content">
         <div class="CaseStudy-contentBody">
           {{.Content}}
         </div>
         <div class="CaseStudy-contentAside">
           <div class="CaseStudy-aboutBlock">
-            <img src="/images/logos/{{.Params.logoSrc}}" class="CaseStudy-aboutBlockImg" alt="{{.Params.company}}">
+            <img src="/images/logos/{{.logoSrc}}" class="CaseStudy-aboutBlockImg" alt="{{.company}}">
             <h3 class="CaseStudy-aboutBlockTitle">
-              About {{.Params.company}}
+              About {{.company}}
             </h3>
-            {{if .Params.description}}
-              <p class="CaseStudy-aboutBlockBody">{{markdown .Params.description}}</p>
+            {{if .description}}
+              <p class="CaseStudy-aboutBlockBody">{{markdown .description}}</p>
             {{ else }}
-              <p class="CaseStudy-aboutBlockBody">{{markdown .Params.quote}}</p>
+              <p class="CaseStudy-aboutBlockBody">{{markdown .quote}}</p>
             {{end}}
           </div>
         </div>
       </div>
-    {{else if (eq .Params.Series "Use Cases") }}
+    {{else if (eq .series "Use Cases") }}
       <div class="UseCase-content">
         <div class="UseCase-contentAside">
           <div
@@ -51,7 +51,7 @@
         </div>
         <div class="UseCase-contentBody js-useCaseContentBody">
           <!-- Messaging -->
-          {{ if .Params.inlineMessage }}
+          {{ if .inlineMessage }}
             <div class="InlineMessage">
               <span class="InlineMessage-imageWrap">
                 <img
@@ -61,14 +61,14 @@
                   alt="Gopher with megaphone">
               </span>
               <span class="InlineMessage-description">
-                <p class="InlineMessage-descParagraph">{{ .Params.inlineMessage.description }}</p>
+                <p class="InlineMessage-descParagraph">{{ .inlineMessage.description }}</p>
               </span>
               <span class="InlineMessage-ctas">
-                <a href="{{ .Params.inlineMessage.ctas.primary.url }}" class="InlineMessage-cta">
-                  {{ .Params.inlineMessage.ctas.primary.title }}
+                <a href="{{ .inlineMessage.ctas.primary.url }}" class="InlineMessage-cta">
+                  {{ .inlineMessage.ctas.primary.title }}
                 </a>
-                <a href="{{ .Params.inlineMessage.ctas.secondary.url }}" class="InlineMessage-cta">
-                  {{ .Params.inlineMessage.ctas.secondary.title }}
+                <a href="{{ .inlineMessage.ctas.secondary.url }}" class="InlineMessage-cta">
+                  {{ .inlineMessage.ctas.secondary.title }}
                 </a>
               </span>
             </div>
diff --git a/go.dev/cmd/internal/site/md.go b/go.dev/cmd/internal/site/md.go
index 21ab43d..5faa799 100644
--- a/go.dev/cmd/internal/site/md.go
+++ b/go.dev/cmd/internal/site/md.go
@@ -43,13 +43,13 @@
 // first applying the template execution engine and then interpreting
 // the result as markdown to be converted to HTML.
 // This is the same logic used by the Go web site.
-func markdownTemplateToHTML(markdown string, p *Page) (template.HTML, error) {
-	t := p.site.clone().New(p.file)
+func (site *Site) markdownTemplateToHTML(markdown string, p *page) (template.HTML, error) {
+	t := site.clone().New(p.file)
 	if err := tmplfunc.Parse(t, string(p.data)); err != nil {
 		return "", err
 	}
 	var buf bytes.Buffer
-	if err := t.Execute(&buf, p); err != nil {
+	if err := t.Execute(&buf, p.params); err != nil {
 		return "", err
 	}
 	return markdownToHTML(buf.String()), nil
diff --git a/go.dev/cmd/internal/site/page.go b/go.dev/cmd/internal/site/page.go
index 1caea40..fcb263f 100644
--- a/go.dev/cmd/internal/site/page.go
+++ b/go.dev/cmd/internal/site/page.go
@@ -12,67 +12,37 @@
 	"strings"
 	"time"
 
-	"golang.org/x/go.dev/cmd/internal/html/template"
 	"golang.org/x/go.dev/cmd/internal/tmplfunc"
 	"gopkg.in/yaml.v3"
 )
 
-// A Page is a single web page.
+// A page is a single web page.
 // It corresponds to some .md file in the content tree.
-// Although page is not exported for use by other Go code,
-// its exported fields and methods are available to templates.
-type Page struct {
-	id      string // page ID (url path excluding site.BaseURL and trailing slash)
-	file    string // .md file for page
-	section string // page section ID
-	parent  string // parent page ID
-	data    []byte // page data (markdown)
-	html    []byte // rendered page (HTML)
-	site    *Site
-
-	// loaded from page metadata, available to templates
-	Aliases     []string
-	Date        anyTime
-	Description string `yaml:"description"`
-	Layout      string `yaml:"layout"`
-	LinkTitle   string `yaml:"linkTitle"`
-	Title       string
-
-	// provided to templates
-	Content template.HTML          `yaml:"-"`
-	Pages   []*Page                `yaml:"-"`
-	Params  map[string]interface{} `yaml:"-"`
+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) {
-	var section string
+func (site *Site) loadPage(file string) (*page, error) {
 	id := strings.TrimPrefix(file, "_content/")
 	if id == "index.md" {
 		id = ""
-		section = ""
 	} else if strings.HasSuffix(id, "/index.md") {
 		id = strings.TrimSuffix(id, "/index.md")
-		section = id
 	} else {
 		id = strings.TrimSuffix(id, ".md")
-		section = path.Dir(id)
-		if section == "." {
-			section = ""
-		}
-	}
-	parent := path.Dir(id)
-	if parent == "." {
-		parent = ""
 	}
 
 	p := site.newPage(id)
 	p.file = file
-	p.section = section
-	p.parent = parent
-	p.Params["Series"] = ""
-	p.Params["series"] = ""
 
 	// Load content, including leading yaml.
 	data, err := ioutil.ReadFile(site.file(file))
@@ -88,11 +58,7 @@
 		}
 		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)
-			}
-			err = yaml.Unmarshal(meta, p)
+			err := yaml.Unmarshal(meta, p.params)
 			if err != nil {
 				return nil, fmt.Errorf("load %s: %v", file, err)
 			}
@@ -114,44 +80,85 @@
 	}
 	p.data = data
 
-	// Set a few defaults.
-	p.Params["Series"] = p.Params["series"]
-	if p.LinkTitle == "" {
-		p.LinkTitle = p.Title
+	// Default linkTitle to title
+	if _, ok := p.params["linkTitle"]; !ok {
+		p.params["linkTitle"] = p.params["title"]
 	}
 
-	// Register aliases.
-	for _, alias := range p.Aliases {
-		site.redirects[strings.Trim(alias, "/")] = p.URL()
+	// 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
+	urlPath := "/" + p.id
+	if strings.HasSuffix(p.file, "/index.md") && p.id != "" {
+		urlPath += "/"
+	}
+	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
+
+	// Register aliases. Needs URL.
+	aliases, _ := p.params["aliases"].([]interface{})
+	for _, alias := range aliases {
+		if a, ok := alias.(string); ok {
+			site.redirects[strings.Trim(a, "/")] = p.params["URL"].(string)
+		}
+	}
 	return p, nil
 }
 
 // renderHTML renders the HTML for the page, leaving it in p.html.
-func (p *Page) renderHTML() error {
-	var err error
-	p.Content, err = markdownTemplateToHTML(string(p.data), p)
+func (site *Site) renderHTML(p *page) error {
+	content, err := site.markdownTemplateToHTML(string(p.data), p)
 	if err != nil {
 		return err
 	}
+	p.params["Content"] = content
 
 	// Load base template.
-	base, err := ioutil.ReadFile(p.site.file("_templates/layouts/site.tmpl"))
+	base, err := ioutil.ReadFile(site.file("_templates/layouts/site.tmpl"))
 	if err != nil {
 		return err
 	}
-	t := p.site.clone().New("_templates/layouts/site.tmpl")
+	t := site.clone().New("_templates/layouts/site.tmpl")
 	if err := tmplfunc.Parse(t, string(base)); err != nil {
 		return err
 	}
 
 	// Load page-specific layout template.
-	layout := p.Layout
+	layout, _ := p.params["layout"].(string)
 	if layout == "" {
 		layout = "default"
 	}
-	data, err := ioutil.ReadFile(p.site.file("_templates/layouts/" + layout + ".tmpl"))
+	data, err := ioutil.ReadFile(site.file("_templates/layouts/" + layout + ".tmpl"))
 	if err != nil {
 		return err
 	}
@@ -160,29 +167,23 @@
 	}
 
 	var buf bytes.Buffer
-	if err := t.Execute(&buf, p); err != nil {
+	if err := t.Execute(&buf, p.params); err != nil {
 		return err
 	}
 	p.html = buf.Bytes()
 	return nil
 }
 
-// An anyTime is a time.Time that accepts any of the anyTimeFormats when unmarshaling.
-type anyTime struct {
-	time.Time
-}
-
-var anyTimeFormats = []string{
+var dateFormats = []string{
 	"2006-01-02",
 	time.RFC3339,
 }
 
-func (t *anyTime) UnmarshalText(data []byte) error {
-	for _, f := range anyTimeFormats {
-		if tt, err := time.Parse(f, string(data)); err == nil {
-			t.Time = tt
-			return nil
+func parseDate(d string) (time.Time, error) {
+	for _, f := range dateFormats {
+		if tt, err := time.Parse(f, d); err == nil {
+			return tt, nil
 		}
 	}
-	return fmt.Errorf("invalid time: %s", data)
+	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
index d5a3486..9abe247 100644
--- a/go.dev/cmd/internal/site/site.go
+++ b/go.dev/cmd/internal/site/site.go
@@ -29,8 +29,7 @@
 	URL   string
 	Title string
 
-	pages     []*Page
-	pagesByID map[string]*Page
+	pagesByID map[string]*page
 	dir       string
 	redirects map[string]string
 	base      *template.Template
@@ -45,7 +44,7 @@
 	site := &Site{
 		dir:       dir,
 		redirects: make(map[string]string),
-		pagesByID: make(map[string]*Page),
+		pagesByID: make(map[string]*page),
 	}
 	if err := site.initTemplate(); err != nil {
 		return nil, err
@@ -75,36 +74,10 @@
 		return nil, fmt.Errorf("loading pages: %v", err)
 	}
 
-	// Assign pages to sections and sort section lists.
-	for _, p := range site.pages {
-		p.Pages = append(p.Pages, p)
-	}
-	for _, p := range site.pages {
-		if parent := site.pagesByID[p.parent]; parent != nil {
-			parent.Pages = append(parent.Pages, p)
-		}
-	}
-	for _, p := range site.pages {
-		pages := p.Pages[1:]
-		sort.Slice(pages, func(i, j int) bool {
-			pi := pages[i]
-			pj := pages[j]
-			if !pi.Date.Equal(pj.Date.Time) {
-				return pi.Date.After(pj.Date.Time)
-			}
-			ti := pi.LinkTitle
-			tj := pj.LinkTitle
-			if ti != tj {
-				return ti < tj
-			}
-			return false
-		})
-	}
-
 	// Now that all pages are loaded and set up, can render all.
 	// (Pages can refer to other pages.)
-	for _, p := range site.pages {
-		if err := p.renderHTML(); err != nil {
+	for _, p := range site.pagesByID {
+		if err := site.renderHTML(p); err != nil {
 			return nil, err
 		}
 	}
@@ -116,13 +89,11 @@
 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{
-		site:   site,
+func (site *Site) newPage(short string) *page {
+	p := &page{
 		id:     short,
-		Params: make(map[string]interface{}),
+		params: make(tPage),
 	}
-	site.pages = append(site.pages, p)
 	site.pagesByID[p.id] = p
 	return p
 }
@@ -140,6 +111,59 @@
 	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) {
diff --git a/go.dev/cmd/internal/site/tmpl.go b/go.dev/cmd/internal/site/tmpl.go
index c76f10d..1b33c12 100644
--- a/go.dev/cmd/internal/site/tmpl.go
+++ b/go.dev/cmd/internal/site/tmpl.go
@@ -6,9 +6,7 @@
 
 import (
 	"fmt"
-	"path/filepath"
 	"reflect"
-	"sort"
 	"strings"
 
 	"golang.org/x/go.dev/cmd/internal/html/template"
@@ -18,15 +16,17 @@
 
 func (site *Site) initTemplate() error {
 	funcs := template.FuncMap{
+		"add":      func(i, j int) int { return i + j },
 		"data":     site.data,
 		"dict":     dict,
 		"first":    first,
-		"list":     list,
 		"markdown": markdown,
+		"newest":   newest,
+		"page":     site.pageByPath,
+		"pages":    site.pagesGlob,
 		"replace":  replace,
 		"rawhtml":  rawhtml,
-		"sort":     sortFn,
-		"where":    where,
+		"trim":     strings.Trim,
 		"yaml":     yamlFn,
 	}
 
@@ -68,12 +68,11 @@
 		}
 		list = list.Elem()
 	}
-	out := reflect.Zero(list.Type())
 
-	for i := 0; i < list.Len() && i < n; i++ {
-		out = reflect.Append(out, list.Index(i))
+	if list.Len() < n {
+		return list
 	}
-	return out
+	return list.Slice(0, n)
 }
 
 func dict(args ...interface{}) map[string]interface{} {
@@ -107,116 +106,6 @@
 	return template.HTML(toString(s))
 }
 
-func sortFn(list reflect.Value, key, asc string) (reflect.Value, error) {
-	out := reflect.Zero(list.Type())
-	var keys []string
-	var perm []int
-	for i := 0; i < list.Len(); i++ {
-		elem := list.Index(i)
-		v, ok := eval(elem, key)
-		if !ok {
-			return reflect.Value{}, fmt.Errorf("no key %s", key)
-		}
-		keys = append(keys, strings.ToLower(v))
-		perm = append(perm, i)
-	}
-	sort.Slice(perm, func(i, j int) bool {
-		return keys[perm[i]] < keys[perm[j]]
-	})
-	for _, i := range perm {
-		out = reflect.Append(out, list.Index(i))
-	}
-	return out, nil
-}
-
-func where(list reflect.Value, key, val string) reflect.Value {
-	out := reflect.Zero(list.Type())
-	for i := 0; i < list.Len(); i++ {
-		elem := list.Index(i)
-		v, ok := eval(elem, key)
-		if ok && v == val {
-			out = reflect.Append(out, elem)
-		}
-	}
-	return out
-}
-
-func eval(elem reflect.Value, key string) (string, bool) {
-	for _, k := range strings.Split(key, ".") {
-		if !elem.IsValid() {
-			return "", false
-		}
-		m := elem.MethodByName(k)
-		if m.IsValid() {
-			elem = m.Call(nil)[0]
-			continue
-		}
-		if elem.Kind() == reflect.Interface || elem.Kind() == reflect.Ptr {
-			if elem.IsNil() {
-				return "", false
-			}
-			elem = elem.Elem()
-		}
-		switch elem.Kind() {
-		case reflect.Struct:
-			elem = elem.FieldByName(k)
-			continue
-		case reflect.Map:
-			elem = elem.MapIndex(reflect.ValueOf(k))
-			continue
-		}
-		return "", false
-	}
-	if !elem.IsValid() {
-		return "", false
-	}
-	if elem.Kind() == reflect.Interface || elem.Kind() == reflect.Ptr {
-		if elem.IsNil() {
-			return "", false
-		}
-		elem = elem.Elem()
-	}
-	if elem.Kind() != reflect.String {
-		return "", false
-	}
-	return elem.String(), true
-}
-
-func (p *Page) CurrentSection() *Page {
-	return p.site.pagesByID[p.Section()]
-}
-
-func (p *Page) IsHome() bool { return p.id == "" }
-
-func (p *Page) Parent() *Page {
-	if p.IsHome() {
-		return nil
-	}
-	return p.site.pagesByID[p.parent]
-}
-
-func (p *Page) URL() string {
-	return strings.TrimRight(p.site.URL, "/") + p.Path()
-}
-
-func (p *Page) Path() string {
-	if p.id == "" {
-		return "/"
-	}
-	if strings.HasSuffix(p.file, "/index.md") {
-		return "/" + p.id + "/"
-	}
-	return "/" + p.id
-}
-
-func (p *Page) Section() string {
-	i := strings.Index(p.section, "/")
-	if i < 0 {
-		return p.section
-	}
-	return p.section[:i]
-}
-
 func yamlFn(s string) (interface{}, error) {
 	var d interface{}
 	if err := yaml.Unmarshal([]byte(s), &d); err != nil {
@@ -224,7 +113,3 @@
 	}
 	return d, nil
 }
-
-func (p *Page) Dir() string {
-	return strings.TrimPrefix(filepath.ToSlash(filepath.Dir(p.file)), "_content")
-}