// Package web maps requests to bundles and writes bytes. It knows content and render, and exposes // neither to them. package web import ( "log/slog" "net/http" "khosra/internal/content" "khosra/internal/render" ) // Handler serves a site. // // One mux entry, because URL shape is the resolver's business rather than the mux's: see resolve. func Handler(site *content.Site, r *render.Renderer) http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /", func(w http.ResponseWriter, req *http.Request) { serve(w, req, site, r) }) return mux } // serve resolves one request and writes its bundle. func serve(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer) { res, ok := resolve(req.URL.Path, site) if !ok { http.NotFound(w, req) return } // A redirect target only exists for a path that resolves, so check the bundle before sending one: // otherwise a nonexistent page answers 301 and confirms nothing. b, served, found := site.Lookup(res.key, res.lang) if res.redirect != "" && (found || res.key == "") { http.Redirect(w, req, res.redirect, http.StatusMovedPermanently) return } if !found { // An alias is a promise that an old URL keeps working, so it answers a permanent redirect to the // canonical one — and only for an alias that exists, so nothing can be probed by 301. if canonical, isAlias := site.Alias(res.key); isAlias { http.Redirect(w, req, content.URL(canonical, res.lang), http.StatusMovedPermanently) return } http.NotFound(w, req) return } out, err := r.Bundle(b, served, site.Variants(res.key)) if err != nil { // A render failure degrades: log it and say nothing more to the client than that it failed // (conventions.md). It must never leak a template or filesystem detail. slog.Error("render failed", "key", b.Key, "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") if _, err := w.Write(out); err != nil { slog.Warn("write failed", "key", b.Key, "err", err) } }