blob: 0e4773dff0c714f6e6709b591f3faa40f8e82b80 [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package site implements generation of content for serving from go.dev.
// It is meant to support a transition from being a Hugo-based web site
// to being a site compatible with x/website.
package site
import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
"golang.org/x/go.dev/cmd/internal/html/template"
"gopkg.in/yaml.v3"
)
// A Site holds metadata about the entire site.
type Site struct {
URL string
Title string
data map[string]interface{}
pages []*Page
pagesByID map[string]*Page
dir string
redirects map[string]string
base *template.Template
}
// Load loads and returns the site in the directory rooted at dir.
func Load(dir string) (*Site, error) {
dir, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
site := &Site{
dir: dir,
redirects: make(map[string]string),
pagesByID: make(map[string]*Page),
}
if err := site.initTemplate(); err != nil {
return nil, err
}
// Read site config.
data, err := ioutil.ReadFile(site.file("data/site.yaml"))
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(data, &site); err != nil {
return nil, fmt.Errorf("parsing data/site.yaml: %v", err)
}
// Load site data files.
// site.data is a directory tree in which each key points at
// either another directory tree (a subdirectory)
// or a parsed yaml file.
site.data = make(map[string]interface{})
root := site.file("data")
err = filepath.Walk(root, func(name string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if name == root {
name = "."
} else {
name = name[len(root)+1:]
}
if info.IsDir() {
site.data[name] = make(map[string]interface{})
return nil
}
if strings.HasSuffix(name, ".yaml") {
data, err := ioutil.ReadFile(filepath.Join(root, name))
if err != nil {
return err
}
var d interface{}
if err := yaml.Unmarshal(data, &d); err != nil {
return fmt.Errorf("unmarshaling %v: %v", name, err)
}
elems := strings.Split(name, "/")
m := site.data
for _, elem := range elems[:len(elems)-1] {
m = m[elem].(map[string]interface{})
}
m[strings.TrimSuffix(elems[len(elems)-1], ".yaml")] = d
}
return nil
})
if err != nil {
return nil, fmt.Errorf("loading data: %v", err)
}
// Load site pages from md files.
err = filepath.Walk(site.file("content"), func(name string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(name, ".md") {
_, err := site.loadPage(name[len(site.file("."))+1:])
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("loading pages: %v", err)
}
// 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)
}
if pi.Weight != pj.Weight {
return pi.Weight > pj.Weight
}
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 {
return nil, err
}
}
return site, nil
}
// file returns the full path to the named file within the site.
func (site *Site) file(name string) string { return filepath.Join(site.dir, name) }
// newPage returns a new page belonging to site.
func (site *Site) newPage(short string) *Page {
p := &Page{
site: site,
id: short,
Params: make(map[string]interface{}),
}
site.pages = append(site.pages, p)
site.pagesByID[p.id] = p
return p
}
// Open returns the content to serve at the given path.
// This function makes Site an http.FileServer, for easy HTTP serving.
func (site *Site) Open(name string) (http.File, error) {
name = strings.TrimPrefix(name, "/")
switch ext := path.Ext(name); ext {
case ".css", ".jpeg", ".jpg", ".js", ".png", ".svg", ".txt":
if f, err := os.Open(site.file("content/" + name)); err == nil {
return f, nil
}
if f, err := os.Open(site.file("static/" + name)); err == nil {
return f, nil
}
// Maybe it is name.hash.ext. Check hash.
// We will stop generating these eventually,
// so it doesn't matter that this is slow.
prefix := name[:len(name)-len(ext)]
hash := path.Ext(prefix)
prefix = prefix[:len(prefix)-len(hash)]
if len(hash) == 1+64 {
file := site.file("assets/" + prefix + ext)
if data, err := ioutil.ReadFile(file); err == nil && fmt.Sprintf(".%x", sha256.Sum256(data)) == hash {
if f, err := os.Open(file); err == nil {
return f, nil
}
}
}
case ".html":
id := strings.TrimSuffix(name, "/index.html")
if name == "index.html" {
id = ""
}
if target := site.redirects[id]; target != "" {
s := fmt.Sprintf(redirectFmt, target)
return &httpFile{strings.NewReader(s), int64(len(s))}, nil
}
if p := site.pagesByID[id]; p != nil {
return &httpFile{bytes.NewReader(p.html), int64(len(p.html))}, nil
}
}
if !strings.HasSuffix(name, ".html") {
if f, err := site.Open(name + "/index.html"); err == nil {
size, err := f.Seek(0, io.SeekEnd)
f.Close()
if err == nil {
return &httpDir{httpFileInfo{"index.html", size, false}, 0}, nil
}
}
}
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
type httpFile struct {
io.ReadSeeker
size int64
}
func (*httpFile) Close() error { return nil }
func (f *httpFile) Stat() (os.FileInfo, error) { return &httpFileInfo{".", f.size, false}, nil }
func (*httpFile) Readdir(count int) ([]os.FileInfo, error) {
return nil, fmt.Errorf("readdir not available")
}
const redirectFmt = `<!DOCTYPE html><html><head><title>%s</title><link rel="canonical" href="%[1]s"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=%[1]s" /></head></html>`
type httpDir struct {
info httpFileInfo
off int // 0 or 1
}
func (*httpDir) Close() error { return nil }
func (*httpDir) Read([]byte) (int, error) { return 0, fmt.Errorf("read not available") }
func (*httpDir) Seek(int64, int) (int64, error) { return 0, fmt.Errorf("seek not available") }
func (*httpDir) Stat() (os.FileInfo, error) { return &httpFileInfo{".", 0, true}, nil }
func (d *httpDir) Readdir(count int) ([]os.FileInfo, error) {
if count == 0 {
return nil, nil
}
if d.off > 0 {
return nil, io.EOF
}
d.off = 1
return []os.FileInfo{&d.info}, nil
}
type httpFileInfo struct {
name string
size int64
dir bool
}
func (info *httpFileInfo) Name() string { return info.name }
func (info *httpFileInfo) Size() int64 { return info.size }
func (info *httpFileInfo) Mode() os.FileMode {
if info.dir {
return os.ModeDir | 0555
}
return 0444
}
func (info *httpFileInfo) ModTime() time.Time { return time.Time{} }
func (info *httpFileInfo) IsDir() bool { return info.dir }
func (info *httpFileInfo) Sys() interface{} { return nil }