render.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  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 string
  64. Commit commitInfo
  65. CommitURL string // empty when the build doesn't match a commit exactly
  66. About template.HTML
  67. // Release notes pages (and the About page's link to them).
  68. Releases []*Release
  69. Release *Release
  70. }
  71. // loadTemplates parses each page template together with the shared layout.
  72. func loadTemplates() (map[string]*template.Template, error) {
  73. funcs := template.FuncMap{
  74. "date": func(t time.Time) string { return t.Format("2 January 2006") },
  75. "isodate": func(t time.Time) string { return t.Format(time.DateOnly) },
  76. }
  77. base, err := template.New("layout.html").Funcs(funcs).ParseFS(templateFS, "templates/layout.html")
  78. if err != nil {
  79. return nil, err
  80. }
  81. pages := map[string]*template.Template{}
  82. for _, name := range []string{"index", "post", "about", "releases", "release", "error"} {
  83. t, err := base.Clone()
  84. if err != nil {
  85. return nil, err
  86. }
  87. if pages[name], err = t.ParseFS(templateFS, "templates/"+name+".html"); err != nil {
  88. return nil, err
  89. }
  90. }
  91. return pages, nil
  92. }
  93. // render executes a page into a buffer first so a template error becomes a
  94. // clean 500 instead of a half-written page.
  95. func (a *app) render(w http.ResponseWriter, status int, page string, data pageData) {
  96. data.Site = a.siteName
  97. data.CSS = "/static/style.css?v=" + a.assets["style.css"]
  98. data.Config = a.config.Get()
  99. var buf bytes.Buffer
  100. if err := a.pages[page].ExecuteTemplate(&buf, "layout.html", data); err != nil {
  101. log.Printf("render %s: %v", page, err)
  102. http.Error(w, "internal server error", http.StatusInternalServerError)
  103. return
  104. }
  105. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  106. w.WriteHeader(status)
  107. buf.WriteTo(w)
  108. }