+{{template "foot" .}}
diff --git a/internal/web/web.go b/internal/web/web.go
index 074d183..b502a4d 100644
--- a/internal/web/web.go
+++ b/internal/web/web.go
@@ -104,6 +104,13 @@ func (h *Handlers) renderPage(w http.ResponseWriter, name string, data any) {
}
}
+// RenderNotFoundPage is the exported not-found renderer used by the api
+// package (#173): the router's NotFound handler renders the main UI with a
+// friendly "Paste ID not found" message, still with HTTP 404.
+func (h *Handlers) RenderNotFoundPage(w http.ResponseWriter, r *http.Request) {
+ h.renderNotFound(w, r, r.URL.Path)
+}
+
// RenderPage is the exported wrapper used by the api package (#4 can pages).
func (h *Handlers) RenderPage(w http.ResponseWriter, name string, data any) {
h.renderPage(w, name, data)
@@ -200,6 +207,30 @@ func (h *Handlers) writeRateLimited(w http.ResponseWriter, retryAfterSecs int) {
w.Write([]byte(`{"error":"rate limit exceeded"}`))
}
+// renderNotFound serves the friendly not-found page (#173): the main UI
+// chrome (topbar, centered card) with a "Paste ID not found" message in the
+// result card, instead of a bare text 404. Still returns HTTP 404 so
+// crawlers/validators see the correct status.
+func (h *Handlers) renderNotFound(w http.ResponseWriter, r *http.Request, id string) {
+ h.renderPageStatus(w, "notfound.html", http.StatusNotFound, map[string]any{"Page": "notfound", "ID": id})
+}
+
+// HandleNotFoundPage serves the friendly not-found page for unknown routes
+// (#173): main UI chrome with a "Paste ID not found" message. Called from the
+// chi NotFound handler in the api package.
+func (h *Handlers) HandleNotFoundPage(w http.ResponseWriter, r *http.Request) {
+ h.renderNotFound(w, r, r.URL.Path)
+}
+
+// renderPageStatus renders a template with an explicit HTTP status code.
+func (h *Handlers) renderPageStatus(w http.ResponseWriter, name string, status int, data any) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(status)
+ if err := h.UI.tmpl.ExecuteTemplate(w, name, data); err != nil {
+ http.Error(w, "template error: "+err.Error(), 500)
+ }
+}
+
func (h *Handlers) renderPaste(w http.ResponseWriter, row *store.PasteRow, justCreated bool, deletionToken string, readsRemaining *int) {
lines := strings.Count(row.Content, "\n") + 1
gutter := ""
@@ -262,11 +293,14 @@ func (h *Handlers) HandlePasteView(w http.ResponseWriter, r *http.Request) {
return
}
if row == nil {
- http.NotFound(w, r)
+ // #173: a missing paste ID gets the main UI with a friendly message,
+ // not a bare text 404 page.
+ h.renderNotFound(w, r, id)
return
}
if row.ExpiresAt.Valid && row.ExpiresAt.Int64 < time.Now().Unix() {
- http.Error(w, "paste expired", 404)
+ // #173: expired pastes render the same friendly not-found UI.
+ h.renderNotFound(w, r, id)
return
}
if row.PasswordHash.Valid {
@@ -333,8 +367,8 @@ func (h *Handlers) HandlePasteView(w http.ResponseWriter, r *http.Request) {
// Just-created first render does not count as a read for the creator.
if !justCreated {
rem, admitted := h.Store.RegisterRead(row, h.ViewerID(r), h.BurnWindowMin())
- if !admitted { // #58: lost the burn claim; do not render content
- http.NotFound(w, r)
+ if !admitted { // #58: lost the burn claim; #173: friendly not-found UI
+ h.renderNotFound(w, r, row.ID)
return
}
h.renderPaste(w, row, false, "", rem)