render.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. package main
  2. import (
  3. "bytes"
  4. "crypto/sha256"
  5. "embed"
  6. "encoding/hex"
  7. "html/template"
  8. "io/fs"
  9. "log"
  10. "net/http"
  11. "strings"
  12. "time"
  13. "github.com/yuin/goldmark"
  14. "github.com/yuin/goldmark/extension"
  15. "github.com/yuin/goldmark/parser"
  16. )
  17. var (
  18. //go:embed templates/*.html
  19. templateFS embed.FS
  20. //go:embed static
  21. staticFS embed.FS
  22. //go:embed about.md
  23. aboutMarkdown []byte
  24. )
  25. // newMarkdown returns a CommonMark renderer with GitHub-style extensions.
  26. // Raw HTML in posts is omitted (goldmark's default).
  27. func newMarkdown() goldmark.Markdown {
  28. return goldmark.New(
  29. goldmark.WithExtensions(extension.GFM),
  30. goldmark.WithParserOptions(parser.WithAutoHeadingID()),
  31. )
  32. }
  33. // assetHashes returns a short content hash for each file in static/, keyed
  34. // by its path under static/. Pages link to static files with ?v=<hash> so a
  35. // changed file gets a new URL and browsers never use a stale cached copy.
  36. func assetHashes() (map[string]string, error) {
  37. hashes := map[string]string{}
  38. err := fs.WalkDir(staticFS, "static", func(path string, d fs.DirEntry, err error) error {
  39. if err != nil || d.IsDir() {
  40. return err
  41. }
  42. b, err := staticFS.ReadFile(path)
  43. if err != nil {
  44. return err
  45. }
  46. sum := sha256.Sum256(b)
  47. hashes[strings.TrimPrefix(path, "static/")] = hex.EncodeToString(sum[:])[:10]
  48. return nil
  49. })
  50. return hashes, err
  51. }
  52. // pageData is passed to every template.
  53. type pageData struct {
  54. Site string
  55. CSS string // stylesheet URL including its content hash
  56. Config siteConfig
  57. Posts []*Post
  58. Post *Post
  59. Newer, Older *Post
  60. Status int
  61. Message string
  62. // About page only.
  63. Version, Commit string
  64. About template.HTML
  65. // Release notes pages (and the About page's link to them).
  66. Releases []*Release
  67. Release *Release
  68. }
  69. // loadTemplates parses each page template together with the shared layout.
  70. func loadTemplates() (map[string]*template.Template, error) {
  71. funcs := template.FuncMap{
  72. "date": func(t time.Time) string { return t.Format("2 January 2006") },
  73. "isodate": func(t time.Time) string { return t.Format(time.DateOnly) },
  74. }
  75. base, err := template.New("layout.html").Funcs(funcs).ParseFS(templateFS, "templates/layout.html")
  76. if err != nil {
  77. return nil, err
  78. }
  79. pages := map[string]*template.Template{}
  80. for _, name := range []string{"index", "post", "about", "releases", "release", "error"} {
  81. t, err := base.Clone()
  82. if err != nil {
  83. return nil, err
  84. }
  85. if pages[name], err = t.ParseFS(templateFS, "templates/"+name+".html"); err != nil {
  86. return nil, err
  87. }
  88. }
  89. return pages, nil
  90. }
  91. // render executes a page into a buffer first so a template error becomes a
  92. // clean 500 instead of a half-written page.
  93. func (a *app) render(w http.ResponseWriter, status int, page string, data pageData) {
  94. data.Site = a.siteName
  95. data.CSS = "/static/style.css?v=" + a.assets["style.css"]
  96. data.Config = a.config.Get()
  97. var buf bytes.Buffer
  98. if err := a.pages[page].ExecuteTemplate(&buf, "layout.html", data); err != nil {
  99. log.Printf("render %s: %v", page, err)
  100. http.Error(w, "internal server error", http.StatusInternalServerError)
  101. return
  102. }
  103. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  104. w.WriteHeader(status)
  105. buf.WriteTo(w)
  106. }