package main import ( "bytes" "crypto/sha256" "embed" "encoding/hex" "html/template" "io/fs" "log" "net/http" "strings" "time" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" ) var ( //go:embed templates/*.html templateFS embed.FS //go:embed static staticFS embed.FS //go:embed about.md aboutMarkdown []byte ) // newMarkdown returns a CommonMark renderer with GitHub-style extensions. // Raw HTML in posts is omitted (goldmark's default). func newMarkdown() goldmark.Markdown { return goldmark.New( goldmark.WithExtensions(extension.GFM), goldmark.WithParserOptions(parser.WithAutoHeadingID()), ) } // assetHashes returns a short content hash for each file in static/, keyed // by its path under static/. Pages link to static files with ?v= so a // changed file gets a new URL and browsers never use a stale cached copy. func assetHashes() (map[string]string, error) { hashes := map[string]string{} err := fs.WalkDir(staticFS, "static", func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return err } b, err := staticFS.ReadFile(path) if err != nil { return err } sum := sha256.Sum256(b) hashes[strings.TrimPrefix(path, "static/")] = hex.EncodeToString(sum[:])[:10] return nil }) return hashes, err } // pageData is passed to every template. type pageData struct { Site string CSS string // stylesheet URL including its content hash Config siteConfig Posts []*Post Post *Post Newer, Older *Post Status int Message string // About page only. Version string Commit commitInfo CommitURL string // empty when the build doesn't match a commit exactly About template.HTML // Release notes pages (and the About page's link to them). Releases []*Release Release *Release } // loadTemplates parses each page template together with the shared layout. func loadTemplates() (map[string]*template.Template, error) { funcs := template.FuncMap{ "date": func(t time.Time) string { return t.Format("2 January 2006") }, "isodate": func(t time.Time) string { return t.Format(time.DateOnly) }, } base, err := template.New("layout.html").Funcs(funcs).ParseFS(templateFS, "templates/layout.html") if err != nil { return nil, err } pages := map[string]*template.Template{} for _, name := range []string{"index", "post", "about", "releases", "release", "error"} { t, err := base.Clone() if err != nil { return nil, err } if pages[name], err = t.ParseFS(templateFS, "templates/"+name+".html"); err != nil { return nil, err } } return pages, nil } // render executes a page into a buffer first so a template error becomes a // clean 500 instead of a half-written page. func (a *app) render(w http.ResponseWriter, status int, page string, data pageData) { data.Site = a.siteName data.CSS = "/static/style.css?v=" + a.assets["style.css"] data.Config = a.config.Get() var buf bytes.Buffer if err := a.pages[page].ExecuteTemplate(&buf, "layout.html", data); err != nil { log.Printf("render %s: %v", page, err) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(status) buf.WriteTo(w) }