add paginated section index pages
The first collection page earns the Query primitive: content.Query{Section, Lang}
with Site.Run, newest first, undated after dated, ties broken by key so the same
query always answers in the same order. No cache signature — nothing caches, and a
signature with no consumer is speculation.
Pagination lives in the path (ADR-0028): page one is the bare listing URL,
/page/1/ redirects to it, and a page past the end is 404 rather than an empty page,
because an empty page is a URL that means nothing. `page` is therefore a reserved
segment inside a section, now recorded in content-model.md.
Two kinds of page means two parsed template sets already — base plus the block that
kind defines — which is ADR-0019's per-type shape arriving by need rather than by
anticipation. A head struct is embedded in both Page and List so base.html has one
contract, and theme-contract.md gains the listing fields.
Bundle gains Date, accepting an unquoted YAML date or an RFC 3339 string, since
yaml.v3 hands back time.Time for one and a string for the other.
Evidence: 12 posts → /posts/ shows 10 with rel=next to /posts/page/2/,
/posts/page/2/ shows 3 with rel=prev to /posts/, ordering is post-12 11 10,
/posts/page/1/ 301s to /posts/, /posts/page/9/ is 404, /bn/posts/ is 200.
This commit is contained in:
@@ -5,6 +5,7 @@ package web
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"khosra/internal/content"
|
||||
"khosra/internal/render"
|
||||
@@ -21,6 +22,34 @@ func Handler(site *content.Site, r *render.Renderer) http.Handler {
|
||||
return mux
|
||||
}
|
||||
|
||||
// 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
|
||||
// end is a 404 rather than an empty page, because an empty page is a URL that means nothing.
|
||||
func serveListing(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer, res resolution) bool {
|
||||
if res.key == "" || strings.Contains(res.key, "/") {
|
||||
return false
|
||||
}
|
||||
items := site.Run(content.Query{Section: res.key, Lang: res.lang})
|
||||
if len(items) == 0 {
|
||||
return false
|
||||
}
|
||||
if res.page > 1 && (res.page-1)*content.PerPage >= len(items) {
|
||||
return false
|
||||
}
|
||||
out, err := r.Listing(res.key, res.lang, items, res.page)
|
||||
if err != nil {
|
||||
slog.Error("listing failed", "section", res.key, "err", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if _, err := w.Write(out); err != nil {
|
||||
slog.Warn("write failed", "section", res.key, "err", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -42,6 +71,9 @@ func serve(w http.ResponseWriter, req *http.Request, site *content.Site, r *rend
|
||||
http.Redirect(w, req, content.URL(canonical, res.lang), http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
if serveListing(w, req, site, r, res) {
|
||||
return
|
||||
}
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user