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:
+34
-3
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"khosra/internal/content"
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
type resolution struct {
|
||||
key string
|
||||
lang string
|
||||
// page is 1 for a bundle or the first listing page, higher for /page/N/.
|
||||
page int
|
||||
// redirect is the canonical path when the request named a non-canonical one. Non-empty means answer
|
||||
// with a permanent redirect and nothing else.
|
||||
redirect string
|
||||
@@ -48,8 +51,36 @@ func resolve(path string, site *content.Site) (resolution, bool) {
|
||||
return resolution{redirect: "/"}, true
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
return resolution{key: key, lang: lang, redirect: content.URL(key, lang)}, true
|
||||
page := 1
|
||||
// A trailing /page/N/ is pagination, not part of the key (ADR-0028). Page one is the bare listing
|
||||
// URL, so /page/1/ is a second spelling and redirects.
|
||||
if rest, n, isPaged := cutPage(key); isPaged {
|
||||
if n == 1 {
|
||||
return resolution{redirect: content.PageURL(rest, lang, 1)}, true
|
||||
}
|
||||
key, page = rest, n
|
||||
}
|
||||
return resolution{key: key, lang: lang}, true
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
return resolution{key: key, lang: lang, page: page, redirect: content.PageURL(key, lang, page)}, true
|
||||
}
|
||||
return resolution{key: key, lang: lang, page: page}, true
|
||||
}
|
||||
|
||||
// cutPage strips a trailing "page/N" off a key, reporting the page number.
|
||||
func cutPage(key string) (rest string, page int, ok bool) {
|
||||
i := strings.LastIndex(key, "/")
|
||||
if i < 0 {
|
||||
return key, 1, false
|
||||
}
|
||||
n, err := strconv.Atoi(key[i+1:])
|
||||
if err != nil || n < 1 {
|
||||
return key, 1, false
|
||||
}
|
||||
switch base := key[:i]; {
|
||||
case base == "page":
|
||||
return "", n, true
|
||||
case strings.HasSuffix(base, "/page"):
|
||||
return strings.TrimSuffix(base, "/page"), n, true
|
||||
}
|
||||
return key, 1, false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user