package blog import ( "embed" "html/template" "log" "net/http" "path/filepath" "strings" ) //go:embed templates/*.html var templateFS embed.FS type pageData struct { Title string Posts []Post Post Post PostList bool ActiveSlug string } func safehtml(s string) template.HTML { return template.HTML(s) } var tmpl *template.Template func init() { var err error tmpl, err = template.New("").Funcs(template.FuncMap{"safehtml": safehtml}).ParseFS(templateFS, "templates/*.html") if err != nil { panic(err) } } func render(w http.ResponseWriter, r *http.Request, data pageData) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if r.Header.Get("HX-Request") == "true" { tmpl.ExecuteTemplate(w, "main-content", data) return } tmpl.ExecuteTemplate(w, "base.html", data) } func RegisterRoutes(mux *http.ServeMux, contentDir string) error { ogDir := filepath.Join(contentDir, "..", "og") store := NewStore() if err := store.Load(contentDir); err != nil { log.Printf("blog: warning: could not load posts from %s: %v", contentDir, err) } store.GenerateOGImages(ogDir) mux.Handle("/blog/og/", http.StripPrefix("/blog/og/", http.FileServer(http.Dir(ogDir)))) mux.HandleFunc("/blog/admin/reload", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } if err := store.Load(contentDir); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } store.GenerateOGImages(ogDir) w.Write([]byte("reloaded\n")) }) mux.HandleFunc("/blog/posts/", func(w http.ResponseWriter, r *http.Request) { slug := strings.TrimPrefix(r.URL.Path, "/blog/posts/") if slug == "" || strings.Contains(slug, "/") { http.NotFound(w, r) return } p, ok := store.Get(slug) if !ok { http.NotFound(w, r) return } render(w, r, pageData{ Title: p.Title + " — Chaosmith Systems", Posts: store.All(), Post: p, }) }) mux.HandleFunc("/blog/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/blog/" { http.NotFound(w, r) return } render(w, r, pageData{ Title: "Chaosmith Systems — Field Notes", Posts: store.All(), PostList: true, }) }) mux.HandleFunc("/blog/feed.xml", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8") w.Write([]byte(generateRSS(store))) }) mux.HandleFunc("/blog/feed.json", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/feed+json; charset=utf-8") w.Write([]byte(generateJSONFeed(store))) }) mux.HandleFunc("/sitemap.xml", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/xml; charset=utf-8") w.Write([]byte(generateSitemap(store))) }) mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte("User-agent: *\nAllow: /\n\nSitemap: https://chaosmith.systems/sitemap.xml\n")) }) log.Printf("blog: loaded %d post(s) from %s, mounted at /blog/", store.Count(), contentDir) return nil } func ServeLanding(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } data := pageData{Title: "Chaosmith Systems"} if r.Header.Get("HX-Request") == "true" { tmpl.ExecuteTemplate(w, "main-content", data) return } tmpl.ExecuteTemplate(w, "base.html", data) }