feat: add custom 404 page with playful inline artwork and prose
This commit is contained in:
@@ -28,6 +28,15 @@ func ProvideRouter(cfg config.Config, htmlRender render.HTMLRender) *gin.Engine
|
|||||||
}
|
}
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(CORSMiddlewareWithConfig(cfg), RequestContextMiddleware(), audit.ContextMiddleware(), RequestLogger(), gin.Recovery())
|
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("/static", "./static")
|
||||||
r.Static("/dist", "./dist")
|
r.Static("/dist", "./dist")
|
||||||
r.GET("/debug/pprof", gin.WrapH(http.DefaultServeMux))
|
r.GET("/debug/pprof", gin.WrapH(http.DefaultServeMux))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"mal/internal/config"
|
"mal/internal/config"
|
||||||
|
"mal/templates"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -182,3 +183,50 @@ func TestRespondErrorIncludesRequestContext(t *testing.T) {
|
|||||||
t.Fatalf("log line missing request route: %s", logLine)
|
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"}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,39 @@
|
|||||||
{{define "title"}}Not Found{{end}}
|
{{define "title"}}Not Found{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="text-center py-20">
|
<section class="flex min-h-[78dvh] items-center justify-center px-4 py-8 sm:px-6 lg:px-8">
|
||||||
<h2 class="text-4xl font-normal mb-4">404</h2>
|
<div class="mx-auto flex w-full max-w-4xl flex-col items-center text-center">
|
||||||
<p class="text-xl text-foreground-muted">Page not found</p>
|
<div class="relative flex aspect-square w-full max-w-110 items-center justify-center overflow-hidden rounded-full bg-background-surface ring-1 ring-black/5">
|
||||||
</div>
|
<div class="absolute inset-8 rounded-full border border-foreground/10"></div>
|
||||||
|
<div class="absolute inset-16 rounded-full border border-foreground/8"></div>
|
||||||
|
<div class="absolute left-12 top-16 h-4 w-4 rounded-full bg-foreground/12"></div>
|
||||||
|
<div class="absolute right-14 top-18 h-3 w-3 rounded-full bg-foreground/18"></div>
|
||||||
|
<div class="absolute left-18 bottom-20 h-2.5 w-2.5 rounded-full bg-foreground/14"></div>
|
||||||
|
<div class="absolute right-18 bottom-16 h-20 w-20 rounded-full border border-foreground/10"></div>
|
||||||
|
<div class="absolute left-16 top-1/2 h-px w-18 -translate-y-1/2 rotate-[-12deg] bg-foreground/18"></div>
|
||||||
|
<div class="absolute right-16 top-1/2 h-px w-18 -translate-y-1/2 rotate-[12deg] bg-foreground/18"></div>
|
||||||
|
<div class="relative flex h-52 w-52 items-center justify-center rounded-full bg-background shadow-sm ring-1 ring-black/5 sm:h-64 sm:w-64">
|
||||||
|
<div class="absolute top-16 left-16 h-4 w-4 rounded-full bg-foreground"></div>
|
||||||
|
<div class="absolute top-16 right-16 h-4 w-4 rounded-full bg-foreground"></div>
|
||||||
|
<div class="absolute bottom-18 h-16 w-28 rounded-b-full border-b-[3px] border-foreground/80"></div>
|
||||||
|
<div class="absolute -left-8 top-1/2 h-px w-14 -translate-y-1/2 bg-foreground/20"></div>
|
||||||
|
<div class="absolute -right-8 top-1/2 h-px w-14 -translate-y-1/2 bg-foreground/20"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="mt-8 max-w-3xl font-[Newsreader] text-5xl leading-none text-foreground sm:text-6xl lg:text-7xl">
|
||||||
|
You got a little lost.
|
||||||
|
</h1>
|
||||||
|
<p class="mt-5 max-w-xl text-lg text-foreground-muted sm:text-xl">
|
||||||
|
This page slipped off the map for a minute. Let's get you back to something worth watching.
|
||||||
|
</p>
|
||||||
|
<div class="mt-9 flex flex-col items-center gap-3 sm:flex-row">
|
||||||
|
<a href="/" class="inline-flex h-11 items-center justify-center rounded-xs bg-[#cccccc] px-6 text-base font-normal text-black transition hover:bg-[#bfbfbf] active:bg-[#b3b3b3]">
|
||||||
|
Head back home
|
||||||
|
</a>
|
||||||
|
<a href="/browse" class="inline-flex h-11 items-center justify-center px-3 text-base text-foreground-muted transition hover:text-foreground">
|
||||||
|
Browse something good
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user