package server import ( "bytes" "log" "net/http" "prose/common" "prose/errorcatcher" "prose/postmap" "prose/stylemap" "prose/tplmap" "prose/watcher" "strings" "github.com/aymerick/raymond" ) type server struct { staticHandler http.Handler templates watcher.AutoMap[string, *raymond.Template] posts watcher.OrderedAutoMap[string, *postmap.Post] styles watcher.AutoMap[string, string] homeImage []byte } func New() error { s := &server{ staticHandler: http.FileServer(http.Dir("static/")), } var imgBuffer bytes.Buffer err := common.CreateImage(common.BlogTitle, common.BlogSummary, common.BlogURL, &imgBuffer) if err != nil { return err } s.homeImage = imgBuffer.Bytes() s.posts, err = postmap.New() if err != nil { return err } s.templates, err = tplmap.New() if err != nil { return err } s.styles, err = stylemap.New() if err != nil { return err } handlers := map[string]http.HandlerFunc{ "GET /{$}": s.serveGetHome, "GET /banner.png": s.serveGetImage(s.homeImage), "GET /rss.xml": s.serveGetRSS, "GET /{slug}": s.serveGetPost, "GET /img/banner/{img}": s.serveGetPostImage, "GET /css/{filename}": s.serveGetStylesheet, "GET /": s.staticHandler.ServeHTTP, } for pattern, handler := range handlers { http.Handle(pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w = errorcatcher.New(w, r, s.templates) handler(w, r) })) } return nil } func (s *server) errorInRequest(res http.ResponseWriter, req *http.Request, err error) { res.WriteHeader(http.StatusInternalServerError) log.Printf("ERR %s: %s", req.URL.Path, err) } func (s *server) createWebPage(title, subtitle, contents, banner string) (string, error) { ctx := map[string]interface{}{ "title": title, "subtitle": subtitle, "contents": contents, "banner": banner, } tpl, _ := s.templates.Get("page.html") return tpl.Exec(ctx) } func (s *server) serveGetPost(w http.ResponseWriter, r *http.Request) { p, ok := s.posts.Get(r.PathValue("slug")) if !ok { w.WriteHeader(http.StatusNotFound) return } w.Header().Add("content-type", "text/html; charset=utf-8") tpl, _ := s.templates.Get("fullpost.html") contents, err := tpl.Exec(p) if err != nil { s.errorInRequest(w, r, err) return } page, err := s.createWebPage(p.Metadata.Title, p.Metadata.Summary, contents, "/img/banner/"+p.Slug+".png") if err != nil { s.errorInRequest(w, r, err) return } w.Write([]byte(page)) } func (s *server) serveGetHome(w http.ResponseWriter, r *http.Request) { w.Header().Add("content-type", "text/html; charset=utf-8") var posts string summaryTpl, _ := s.templates.Get("summary.html") for _, p := range s.posts.All() { summary, err := summaryTpl.Exec(p) if err != nil { log.Printf("could not render post summary for %s", p.Slug) } posts = posts + summary } page, err := s.createWebPage("Home", common.BlogSummary, posts, "/banner.png") if err != nil { s.errorInRequest(w, r, err) return } w.Write([]byte(page)) } func (s *server) serveGetPostImage(w http.ResponseWriter, r *http.Request) { slug, ok := strings.CutSuffix(r.PathValue("img"), ".png") if !ok { w.WriteHeader(http.StatusNotFound) return } post, ok := s.posts.Get(slug) if !ok { w.WriteHeader(http.StatusNotFound) return } s.serveGetImage(post.Image)(w, r) } func (s *server) serveGetImage(img []byte) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Add("content-type", "image/png") w.Write(img) } } func (s *server) serveGetRSS(w http.ResponseWriter, r *http.Request) { w.Header().Add("content-type", "text/xml; charset=utf-8") var posts, pubDate string rssItemTpl, _ := s.templates.Get("rss-item.xml") for _, p := range s.posts.All() { if pubDate != "" { pubDate = common.RSSDatetime(p.Metadata.Time) } summary, err := rssItemTpl.Exec(p) if err != nil { log.Printf("could not render post summary for %s", p.Slug) } posts = posts + summary } if pubDate == "" { pubDate = common.RSSDatetime(0) } rssPageTpl, _ := s.templates.Get("rss-channel.xml") page, err := rssPageTpl.Exec(map[string]string{ "title": common.BlogTitle, "description": common.BlogSummary, "link": common.BlogURL, "pubDate": pubDate, "items": posts, }) if err != nil { s.errorInRequest(w, r, err) return } w.Write([]byte(page)) } func (s *server) serveGetStylesheet(w http.ResponseWriter, r *http.Request) { contents, ok := s.styles.Get(r.PathValue("filename")) if !ok { w.WriteHeader(http.StatusNotFound) return } w.Header().Add("content-type", "text/css") w.Write([]byte(contents)) }