| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- 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=<hash> 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)
- }
|