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:
+26
-1
@@ -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
|
||||
|
||||
@@ -22,11 +22,11 @@ func testHandler(t *testing.T) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := render.New()
|
||||
r, err := render.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(content.NewSite(bundles), r)
|
||||
return Handler(content.NewSite(bundles), r, fsys)
|
||||
}
|
||||
|
||||
func TestServeBundleAtItsPermalink(t *testing.T) {
|
||||
@@ -82,11 +82,11 @@ func multilingualHandler(t *testing.T) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := render.New()
|
||||
r, err := render.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(content.NewSite(bundles), r)
|
||||
return Handler(content.NewSite(bundles), r, fsys)
|
||||
}
|
||||
|
||||
func TestPrefixedLanguageServesThatVariant(t *testing.T) {
|
||||
@@ -173,11 +173,11 @@ func aliasHandler(t *testing.T) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := render.New()
|
||||
r, err := render.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(content.NewSite(bundles), r)
|
||||
return Handler(content.NewSite(bundles), r, fsys)
|
||||
}
|
||||
|
||||
func TestAliasRedirectsToCanonical(t *testing.T) {
|
||||
@@ -220,11 +220,11 @@ func listingHandler(t *testing.T, n int) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := render.New()
|
||||
r, err := render.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(content.NewSite(bundles), r)
|
||||
return Handler(content.NewSite(bundles), r, fsys)
|
||||
}
|
||||
|
||||
func TestSectionIndexListsNewestFirst(t *testing.T) {
|
||||
@@ -289,3 +289,33 @@ func TestPagePastTheEndIs404(t *testing.T) {
|
||||
t.Errorf("got %d, want 404: an empty page is a URL that means nothing", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFilesAreServedAndDirectoriesAreNot(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"content/pages/about.md": {Data: []byte("---\ntitle: About\n---\nx\n")},
|
||||
"static/style.css": {Data: []byte("body{}")},
|
||||
"static/img/logo.svg": {Data: []byte("<svg/>")},
|
||||
}
|
||||
bundles, err := content.Scan(fsys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := render.New(fsys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := Handler(content.NewSite(bundles), r, fsys)
|
||||
for path, want := range map[string]int{
|
||||
"/static/style.css": http.StatusOK,
|
||||
"/static/img/logo.svg": http.StatusOK,
|
||||
"/static/": http.StatusNotFound,
|
||||
"/static/img/": http.StatusNotFound,
|
||||
"/static/nope.css": http.StatusNotFound,
|
||||
} {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
if rec.Code != want {
|
||||
t.Errorf("GET %s = %d, want %d", path, rec.Code, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user