let a site root override the theme, and serve static/

A site's templates/ is parsed after the embedded set, so the last definition of a
name wins and a theme redefines one block while inheriting the document
(ADR-0019). Per kind exactly two files are overlaid — base.html and that kind's
block — because overlaying every site template into every set lets a listing's
"main" leak into bundle pages, which is the collision per-kind sets exist to
prevent. Both directions are tested.

templates/theme.css in the site root replaces the reference stylesheet outright;
there is no merging to reason about. static/ is served verbatim under /static/,
through the same os.Root-backed fs.FS, and directory paths answer 404 so it never
indexes its own contents.

Queue entries 6 and 7 are deferred again with triggers: the cascade's consumers are
stage toggles, view selection and cache flags — none of which exist — and declared
types are read by ordering, feeds and check, none of which have landed. Building
either now is the speculation that justified withdrawing them.

Evidence: a real site override renders <section class="mine"> inside the embedded
document with canonical intact, the listing still uses the embedded block, and
/static/site.css and /static/img/logo.svg are 200 while /static/ and /static/img/
are 404.
This commit is contained in:
2026-08-01 02:23:34 +06:00
parent 60a7e10aee
commit 61af326474
7 changed files with 204 additions and 29 deletions
+26 -1
View File
@@ -3,6 +3,7 @@
package web
import (
"io/fs"
"log/slog"
"net/http"
"strings"
@@ -14,14 +15,38 @@ import (
// 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 {
func Handler(site *content.Site, r *render.Renderer, siteFS fs.FS) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /", func(w http.ResponseWriter, req *http.Request) {
serve(w, req, site, r)
})
if siteFS != nil {
mux.Handle("GET /static/", http.StripPrefix("/static/", noListing(staticFS(siteFS))))
}
return mux
}
// staticFS serves the site root's static/ directory verbatim. It keeps the os.Root guarantee, because
// the fs.FS it is given is the one rooted there (ADR-0031).
func staticFS(siteFS fs.FS) http.Handler {
sub, err := fs.Sub(siteFS, "static")
if err != nil {
return http.NotFoundHandler()
}
return http.FileServerFS(sub)
}
// noListing refuses directory paths, so static/ never answers with an index of its own contents.
func noListing(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "" || strings.HasSuffix(req.URL.Path, "/") {
http.NotFound(w, req)
return
}
h.ServeHTTP(w, req)
})
}
// serveListing answers a section index, reporting whether it handled the request.
//
// A section is not a bundle, so this runs only after the bundle lookup misses. A page number past the