xesite/cmd/site/html.go

246 lines
5.5 KiB
Go

package main
import (
"context"
"fmt"
"html/template"
"net/http"
"path/filepath"
"strings"
"time"
"christine.website/cmd/site/internal"
"christine.website/cmd/site/internal/blog"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"within.website/ln"
"within.website/ln/opname"
)
var (
templateRenderTime = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "template_render_time",
Help: "Template render time in nanoseconds",
}, []string{"name"})
)
func logTemplateTime(ctx context.Context, name string, f ln.F, from time.Time) {
dur := time.Since(from)
templateRenderTime.With(prometheus.Labels{"name": name}).Observe(float64(dur))
ln.Log(ctx, f, ln.F{"dur": dur, "name": name})
}
func (s *Site) renderTemplatePage(templateFname string, data interface{}) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := opname.With(r.Context(), "renderTemplatePage")
fetag := "W/" + internal.Hash(templateFname, etag) + "-1"
f := ln.F{"etag": fetag, "if_none_match": r.Header.Get("If-None-Match")}
if r.Header.Get("If-None-Match") == fetag {
http.Error(w, "Cached data OK", http.StatusNotModified)
ln.Log(ctx, f, ln.Info("Cache hit"))
return
}
defer logTemplateTime(ctx, templateFname, f, time.Now())
var t *template.Template
var err error
t, err = template.ParseFiles("templates/base.html", "templates/"+templateFname)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
ln.Error(ctx, err, ln.F{"action": "renderTemplatePage", "page": templateFname})
fmt.Fprintf(w, "error: %v", err)
}
w.Header().Set("ETag", fetag)
w.Header().Set("Cache-Control", "max-age=432000")
err = t.Execute(w, data)
if err != nil {
panic(err)
}
})
}
var postView = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "posts_viewed",
Help: "The number of views per post or talk",
}, []string{"base"})
func (s *Site) listSeries(w http.ResponseWriter, r *http.Request) {
s.renderTemplatePage("series.html", s.Series).ServeHTTP(w, r)
}
func (s *Site) showSeries(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "/blog/series/" {
http.Redirect(w, r, "/blog/series", http.StatusSeeOther)
return
}
series := filepath.Base(r.URL.Path)
var posts []blog.Post
for _, p := range s.Posts {
if p.Series == series {
posts = append(posts, p)
}
}
s.renderTemplatePage("serieslist.html", struct {
Name string
Posts []blog.Post
}{
Name: series,
Posts: posts,
}).ServeHTTP(w, r)
}
func (s *Site) showGallery(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "/gallery/" {
http.Redirect(w, r, "/gallery", http.StatusSeeOther)
return
}
cmp := r.URL.Path[1:]
var p blog.Post
var found bool
for _, pst := range s.Gallery {
if pst.Link == cmp {
p = pst
found = true
}
}
if !found {
w.WriteHeader(http.StatusNotFound)
s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r)
return
}
var tags string
if len(p.Tags) != 0 {
for _, t := range p.Tags {
tags = tags + " #" + strings.ReplaceAll(t, "-", "")
}
}
h := s.renderTemplatePage("gallerypost.html", struct {
Title string
Link string
BodyHTML template.HTML
Date string
Tags string
Image string
}{
Title: p.Title,
Link: p.Link,
BodyHTML: p.BodyHTML,
Date: internal.IOS13Detri(p.Date),
Tags: tags,
Image: p.ImageURL,
})
if h == nil {
panic("how did we get here?")
}
h.ServeHTTP(w, r)
postView.With(prometheus.Labels{"base": filepath.Base(p.Link)}).Inc()
}
func (s *Site) showTalk(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "/talks/" {
http.Redirect(w, r, "/talks", http.StatusSeeOther)
return
}
cmp := r.URL.Path[1:]
var p blog.Post
var found bool
for _, pst := range s.Talks {
if pst.Link == cmp {
p = pst
found = true
}
}
if !found {
w.WriteHeader(http.StatusNotFound)
s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r)
return
}
h := s.renderTemplatePage("talkpost.html", struct {
Title string
Link string
BodyHTML template.HTML
Date string
SlidesLink string
}{
Title: p.Title,
Link: p.Link,
BodyHTML: p.BodyHTML,
Date: internal.IOS13Detri(p.Date),
SlidesLink: p.SlidesLink,
})
if h == nil {
panic("how did we get here?")
}
h.ServeHTTP(w, r)
postView.With(prometheus.Labels{"base": filepath.Base(p.Link)}).Inc()
}
func (s *Site) showPost(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "/blog/" {
http.Redirect(w, r, "/blog", http.StatusSeeOther)
return
}
cmp := r.URL.Path[1:]
var p blog.Post
var found bool
for _, pst := range s.Posts {
if pst.Link == cmp {
p = pst
found = true
}
}
if !found {
w.WriteHeader(http.StatusNotFound)
s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r)
return
}
var tags string
if len(p.Tags) != 0 {
for _, t := range p.Tags {
tags = tags + " #" + strings.ReplaceAll(t, "-", "")
}
}
s.renderTemplatePage("blogpost.html", struct {
Title string
Link string
BodyHTML template.HTML
Date string
Series, SeriesTag string
Tags string
}{
Title: p.Title,
Link: p.Link,
BodyHTML: p.BodyHTML,
Date: internal.IOS13Detri(p.Date),
Series: p.Series,
SeriesTag: strings.ReplaceAll(p.Series, "-", ""),
Tags: tags,
}).ServeHTTP(w, r)
postView.With(prometheus.Labels{"base": filepath.Base(p.Link)}).Inc()
}