package handlers import ( "encoding/json" "log/slog" "net/http" ) // NotFoundHandler handles 404 errors type NotFoundHandler struct { BaseUIHandler } func (h *NotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { RenderNotFound(w, r, &h.BaseUIHandler) } // RenderNotFound renders the 404 page template. // Use this from other handlers when a resource is not found. func RenderNotFound(w http.ResponseWriter, r *http.Request, h *BaseUIHandler) { w.WriteHeader(http.StatusNotFound) meta := NewPageMeta( "404 - Lost at Sea | "+h.ClientShortName, "Page not found - the requested resource doesn't exist on "+h.ClientShortName, ).WithRobots("noindex"). WithSiteName(h.ClientShortName) data := struct { PageData Meta *PageMeta }{ PageData: NewPageData(r, h), Meta: meta, } if err := h.Templates.ExecuteTemplate(w, "404", data); err != nil { http.Error(w, "Page not found", http.StatusNotFound) } } // RenderHTMXError sends an error response suitable for htmx. For htmx requests // it sets an HX-Trigger header so the client fires a toast event; the JS // fallback in app.js will show a generic toast even without the header. // For non-htmx requests it falls back to http.Error. serverErr is logged but // never exposed to the user — pass userMsg for anything screen-readable. func RenderHTMXError(w http.ResponseWriter, r *http.Request, status int, userMsg string, serverErr error) { if serverErr != nil { slog.Error("htmx handler error", "path", r.URL.Path, "status", status, "err", serverErr, ) } if userMsg == "" { userMsg = http.StatusText(status) } if r.Header.Get("HX-Request") == "true" { trigger := map[string]map[string]string{ "toast": {"message": userMsg, "type": "error"}, } if b, err := json.Marshal(trigger); err == nil { w.Header().Set("HX-Trigger", string(b)) } w.Header().Set("HX-Reswap", "none") w.WriteHeader(status) return } http.Error(w, userMsg, status) }