// Kontra — news-opinion site. Single static binary. // Serves markdown content as HTML per request (GOTH stack). package main import ( "embed" "log" "mime" "net/http" "os" ) // bundled static assets + admin UI (served under /web/) //go:embed web var web embed.FS func main() { // Decap CMS requires config.yml served as a YAML content type (it checks the header). _ = mime.AddExtensionType(".yml", "application/yaml") _ = mime.AddExtensionType(".yaml", "application/yaml") root := os.Getenv("KONTRA_CONTENT") if root == "" { root = "content" } mediaBase := os.Getenv("KONTRA_MEDIA") if mediaBase == "" { mediaBase = "/media" } c, err := NewContent(root, mediaBase) if err != nil { log.Fatalf("failed to load content from %s: %v", root, err) } log.Printf("loaded %d articles across %d subjects", len(c.Articles), len(c.Subjects)) s := NewSite(c) // media (Garage S3 via minio-go SDK) s.Media = NewMediaClient() // Routes — register specific paths BEFORE the catch-all. http.HandleFunc("/media/{path...}", func(w http.ResponseWriter, r *http.Request) { s.media(w, r, r.PathValue("path")) }) http.HandleFunc("/admin/media/upload", func(w http.ResponseWriter, r *http.Request) { s.mediaUpload(w, r) }) http.HandleFunc("/subjects/{slug}", func(w http.ResponseWriter, r *http.Request) { s.subject(w, r, r.PathValue("slug")) }) http.HandleFunc("/articles/{slug}", func(w http.ResponseWriter, r *http.Request) { s.article(w, r, r.PathValue("slug")) }) http.Handle("/web/", http.FileServerFS(web)) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { s.home(w, r) }) port := os.Getenv("PORT") if port == "" { port = "8080" } addr := ":" + port log.Printf("Kontra listening on %s (content: %s)", addr, root) if err := http.ListenAndServe(addr, nil); err != nil { log.Fatalf("server error: %v", err) } }