// 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" "strings" "khosra/internal/content" "khosra/internal/render" ) // Handler serves a site. // // One routing case for now: a path is a bundle key. Extracting a resolver waits for the second case, // which language routing brings — check the counter in docs/state.md rather than anticipating it. 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. // // The canonical form of every bundle URL ends in a slash (ADR-0008), so a slashless path that names a // bundle redirects permanently rather than serving a second URL for the same content. func serve(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer) { p := req.URL.Path if p == "/" { http.NotFound(w, req) return } key := content.Normalise(strings.Trim(p, "/")) if !strings.HasSuffix(p, "/") { if _, ok := site.Lookup(key); ok { http.Redirect(w, req, p+"/", http.StatusMovedPermanently) return } http.NotFound(w, req) return } b, ok := site.Lookup(key) if !ok { http.NotFound(w, req) return } out, err := r.Bundle(b) 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) } }