feat: pagination forms and suffixes in resolve and site

- Pagination tab: query or path form, per-locale segment, title and
  description suffixes with %page% and %pages%
- ?page=1, /<segment>/1 and the other form answer 301 to the configured
  address in one hop with normalisation and strict URLs; a page value
  that is not a number answers not_found with pagination.invalid
- page 2 and later: self canonical with the page, index,follow, the
  suffixes (%pages% left for the layer), paged hreflang alternates
- GET /site answers pagination {form, param, segments} and home_crumb
This commit is contained in:
Jakub Zych
2026-11-04 14:22:21 +01:00
parent fde5024b5a
commit 032d3dae45
12 changed files with 763 additions and 144 deletions

View File

@@ -15,7 +15,8 @@ import (
// sitemap settings; Routes each content type's layered URL pattern for
// the layer's drift check; RootFiles the file names the layer serves at
// the site root (verification files, the IndexNow key file); LLMS whether
// /llms.txt is served.
// /llms.txt is served; Pagination the site's page address form and
// HomeCrumb the home crumb (33-06, D-40, D-86).
type SiteAnswer struct {
SiteURL string `json:"site_url"`
DefaultLocale string `json:"default_locale"`
@@ -26,6 +27,25 @@ type SiteAnswer struct {
Routes []SiteRoute `json:"routes"`
RootFiles []string `json:"root_files"`
LLMS bool `json:"llms"`
Pagination SitePagination `json:"pagination"`
HomeCrumb SiteHomeCrumb `json:"home_crumb"`
}
// SitePagination is the site answer's pagination block: the form (query
// or path), the query parameter and the path segment per locale, so the
// layer builds numbered links and checks drift.
type SitePagination struct {
Form string `json:"form"`
Param string `json:"param"`
Segments map[string]string `json:"segments"`
}
// SiteHomeCrumb is the home crumb: its name in the default locale, the
// home URL and the name per locale.
type SiteHomeCrumb struct {
Name string `json:"name"`
URL string `json:"url"`
Names map[string]string `json:"names"`
}
// SiteLinks is the site answer's links block.
@@ -86,6 +106,13 @@ func (s *Service) Site(ctx context.Context) (SiteAnswer, error) {
out.Locales = s.Locales(ctx, db)
out.Links.NewTab = settings.LinksNewTab
out.LLMS = robots.LlmsEnabled
pages := s.PageConfigOf(ctx, db, settings)
out.Pagination = SitePagination{Form: pages.Form, Param: PageParam, Segments: pages.Segments}
out.HomeCrumb = SiteHomeCrumb{URL: out.SiteURL + "/", Names: map[string]string{}}
for _, locale := range out.Locales {
out.HomeCrumb.Names[locale] = s.HomeCrumbName(ctx, db, settings, locale)
}
out.HomeCrumb.Name = out.HomeCrumb.Names[out.DefaultLocale]
if reg := s.Registry(); reg != nil {
for _, typ := range reg.ContentTypes() {
if typ.Key == PageTypeKey {