From 0d31d46fae5917a0ae51c084445f0ee8139a5469 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Fri, 26 Jun 2026 15:12:10 +0200 Subject: [PATCH] feat: add custom 404 page with playful inline artwork and prose --- internal/server/server.go | 9 +++++++ internal/server/server_test.go | 48 ++++++++++++++++++++++++++++++++++ templates/not_found.gohtml | 40 +++++++++++++++++++++++++--- templates/snapshot_test.go | 23 ++++++++++++++++ 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index ba2ddbab..efc2f030 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -28,6 +28,15 @@ func ProvideRouter(cfg config.Config, htmlRender render.HTMLRender) *gin.Engine } r := gin.New() r.Use(CORSMiddlewareWithConfig(cfg), RequestContextMiddleware(), audit.ContextMiddleware(), RequestLogger(), gin.Recovery()) + r.NoRoute(func(c *gin.Context) { + if acceptsHTML(c) { + c.HTML(http.StatusNotFound, "not_found.gohtml", gin.H{ + "CurrentPath": c.Request.URL.Path, + }) + return + } + c.JSON(http.StatusNotFound, ErrorResponse{Error: "Not found"}) + }) r.Static("/static", "./static") r.Static("/dist", "./dist") r.GET("/debug/pprof", gin.WrapH(http.DefaultServeMux)) diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 97c8d7ca..a4b08b56 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -6,6 +6,7 @@ import ( "io" "log" "mal/internal/config" + "mal/templates" "net/http" "net/http/httptest" "strings" @@ -182,3 +183,50 @@ func TestRespondErrorIncludesRequestContext(t *testing.T) { t.Fatalf("log line missing request route: %s", logLine) } } + +func TestProvideRouterRendersNotFoundPageForHTMLRequests(t *testing.T) { + gin.SetMode(gin.TestMode) + + renderer, err := templates.ProvideRenderer() + if err != nil { + t.Fatalf("ProvideRenderer: %v", err) + } + + router := ProvideRouter(config.Config{GinMode: gin.TestMode}, renderer) + req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/missing", nil) + req.Header.Set("Accept", "text/html") + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound) + } + for _, want := range []string{"You got a little lost", "This page slipped off the map for a minute", "Head back home"} { + if !strings.Contains(rec.Body.String(), want) { + t.Fatalf("not found page missing %q in body:\n%s", want, rec.Body.String()) + } + } +} + +func TestProvideRouterReturnsJSONForAPINotFound(t *testing.T) { + gin.SetMode(gin.TestMode) + + renderer, err := templates.ProvideRenderer() + if err != nil { + t.Fatalf("ProvideRenderer: %v", err) + } + + router := ProvideRouter(config.Config{GinMode: gin.TestMode}, renderer) + req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/api/missing", nil) + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound) + } + if got := rec.Body.String(); got != `{"error":"Not found"}` { + t.Fatalf("body = %q, want %q", got, `{"error":"Not found"}`) + } +} diff --git a/templates/not_found.gohtml b/templates/not_found.gohtml index cf80ff3e..2aeddcf1 100644 --- a/templates/not_found.gohtml +++ b/templates/not_found.gohtml @@ -1,7 +1,39 @@ {{define "title"}}Not Found{{end}} {{define "content"}} -
-

404

-

Page not found

-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +

+ You got a little lost. +

+

+ This page slipped off the map for a minute. Let's get you back to something worth watching. +

+ +
+
{{end}} diff --git a/templates/snapshot_test.go b/templates/snapshot_test.go index 1b07aef5..ab872097 100644 --- a/templates/snapshot_test.go +++ b/templates/snapshot_test.go @@ -29,3 +29,26 @@ func TestLoginTemplateShowsCoreCopy(t *testing.T) { } } } + +func TestNotFoundTemplateShowsCoreCopy(t *testing.T) { + r, err := ProvideRenderer() + if err != nil { + t.Fatalf("ProvideRenderer: %v", err) + } + + var buf bytes.Buffer + if err := r.ExecuteFragment(&buf, "not_found.gohtml", "content", map[string]any{}); err != nil { + t.Fatalf("ExecuteFragment: %v", err) + } + + body := buf.String() + for _, want := range []string{ + "You got a little lost", + "This page slipped off the map for a minute", + "Head back home", + } { + if !strings.Contains(body, want) { + t.Fatalf("not found template missing %q in output:\n%s", want, body) + } + } +}