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:
@@ -477,3 +477,124 @@ func TestPublisherSettings(t *testing.T) {
|
||||
t.Fatalf("home crumb trail = %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// expectMoved asserts a 301 answer to location (one hop).
|
||||
func expectMoved(t *testing.T, env *testEnv, target, location string) {
|
||||
t.Helper()
|
||||
res := env.resolve(t, target)
|
||||
if res["action"] != "redirect" || res["status"] != float64(301) || res["location"] != siteURL+location {
|
||||
t.Fatalf("%s = %v %v %v, want 301 %s", target, res["action"], res["status"], res["location"], location)
|
||||
}
|
||||
if next := env.resolve(t, location); next["action"] != "ok" {
|
||||
t.Fatalf("%s → %s answers %v, want ok (one hop)", target, location, next["action"])
|
||||
}
|
||||
}
|
||||
|
||||
// expectInvalidPage asserts a not_found answer with pagination.invalid.
|
||||
func expectInvalidPage(t *testing.T, env *testEnv, target string) {
|
||||
t.Helper()
|
||||
res := env.resolve(t, target)
|
||||
p, _ := res["pagination"].(map[string]any)
|
||||
if res["action"] != "not_found" || p == nil || p["invalid"] != true {
|
||||
t.Fatalf("%s = %v pagination %v, want not_found with invalid", target, res["action"], res["pagination"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestPagination covers the page address forms (D-40 to D-42, D-21,
|
||||
// P33-24): 301s to the configured form in one hop, invalid values, page 2
|
||||
// and later with index,follow, a self canonical and the suffixes, and the
|
||||
// site answer.
|
||||
func TestPagination(t *testing.T) {
|
||||
env := bootBlog(t)
|
||||
token := env.admin(t, true)
|
||||
const list = "/blog/category/recipes"
|
||||
|
||||
t.Run("query form", func(t *testing.T) {
|
||||
expectMoved(t, env, list+"?page=1", list)
|
||||
res := env.resolve(t, list+"?page=3")
|
||||
seo := res["seo"].(map[string]any)
|
||||
if res["action"] != "ok" || res["url"] != siteURL+list+"?page=3" || seo["canonical"] != siteURL+list+"?page=3" || seo["robots"] != "index, follow" {
|
||||
t.Fatalf("page 3 = %v", res)
|
||||
}
|
||||
if seo["title"] != "Recipes – page 3 of %pages% | Acme Blog" || seo["description"] != "Page 3 of %pages%." {
|
||||
t.Fatalf("page 3 title %q description %q", seo["title"], seo["description"])
|
||||
}
|
||||
if got := asJSON(t, res["pagination"]); got != `{"form":"query","invalid":false,"page":3,"param":"page","segment":""}` {
|
||||
t.Fatalf("pagination = %s", got)
|
||||
}
|
||||
pl := env.resolve(t, "/pl/blog/kategoria/przepisy?page=3")
|
||||
if title := pl["seo"].(map[string]any)["title"]; title != "Przepisy – strona 3 z %pages% | Acme Blog" {
|
||||
t.Fatalf("pl page 3 title = %v", title)
|
||||
}
|
||||
for _, bad := range []string{"?page=abc", "?page=0", "?page=-1"} {
|
||||
expectInvalidPage(t, env, list+bad)
|
||||
}
|
||||
expectMoved(t, env, list+"/page/3", list+"?page=3")
|
||||
expectMoved(t, env, "/pl/blog/kategoria/przepisy/strona/3", "/pl/blog/kategoria/przepisy?page=3")
|
||||
if res := env.resolve(t, list); res["pagination"] != nil {
|
||||
t.Fatalf("unnumbered page carries pagination: %v", res["pagination"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("one hop with normalisation", func(t *testing.T) {
|
||||
env.expect(t, http.StatusOK, http.MethodPut, "/_seo/api/v1/settings", token, map[string]any{"url_lowercase": true})
|
||||
expectMoved(t, env, "/Blog/Category/Recipes?page=1", list)
|
||||
expectMoved(t, env, "/Blog/Category/Recipes/page/4", list+"?page=4")
|
||||
env.expect(t, http.StatusOK, http.MethodPut, "/_seo/api/v1/settings", token, map[string]any{"url_lowercase": false})
|
||||
})
|
||||
|
||||
t.Run("path form", func(t *testing.T) {
|
||||
env.expect(t, http.StatusOK, http.MethodPut, "/_seo/api/v1/settings", token, map[string]any{"page_form": "path"})
|
||||
expectMoved(t, env, list+"?page=3", list+"/page/3")
|
||||
expectMoved(t, env, list+"/page/1", list)
|
||||
expectInvalidPage(t, env, list+"/page/x")
|
||||
expectInvalidPage(t, env, list+"?page=abc")
|
||||
res := env.resolve(t, list+"/page/3")
|
||||
if res["action"] != "ok" || res["url"] != siteURL+list+"/page/3" || res["seo"].(map[string]any)["canonical"] != siteURL+list+"/page/3" {
|
||||
t.Fatalf("path page 3 = %v", res)
|
||||
}
|
||||
if got := asJSON(t, res["pagination"]); got != `{"form":"path","invalid":false,"page":3,"param":"page","segment":"page"}` {
|
||||
t.Fatalf("pagination = %s", got)
|
||||
}
|
||||
if pl := env.resolve(t, "/pl/blog/kategoria/przepisy/strona/2"); pl["action"] != "ok" || pl["pagination"].(map[string]any)["segment"] != "strona" {
|
||||
t.Fatalf("pl path page = %v", pl)
|
||||
}
|
||||
expectMoved(t, env, "/pl/blog/kategoria/przepisy/page/2", "/pl/blog/kategoria/przepisy/strona/2")
|
||||
// A record whose own address ends like a page stays a record.
|
||||
if res := env.resolve(t, "/blog/first-post"); res["action"] != "ok" {
|
||||
t.Fatalf("post = %v", res["action"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TestSiteEndpoint", func(t *testing.T) {
|
||||
site := env.expect(t, http.StatusOK, http.MethodGet, "/_seo/api/v1/site", "", nil).Body.String()
|
||||
for _, want := range []string{`"pagination":{"form":"path","param":"page","segments":{"en":"page","pl":"strona"}}`, `"home_crumb":{"name":"Home","url":"https://www.example.test/","names":{"en":"Home","pl":"Strona główna"}}`} {
|
||||
if !jsonContains(site, want) {
|
||||
t.Fatalf("site answer lacks %s: %s", want, site)
|
||||
}
|
||||
}
|
||||
env.expect(t, http.StatusOK, http.MethodPut, "/_seo/api/v1/settings", token, map[string]any{"breadcrumb_home": "Start", "page_segment": "p"})
|
||||
site = env.expect(t, http.StatusOK, http.MethodGet, "/_seo/api/v1/site", "", nil).Body.String()
|
||||
if !jsonContains(site, `"home_crumb":{"name":"Start"`) || !jsonContains(site, `"segments":{"en":"p"`) {
|
||||
t.Fatalf("site answer after the change: %s", site)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("refusals", func(t *testing.T) {
|
||||
refusal(t, env.call(t, http.MethodPut, adminAPI("/golem15/seo/settings/1"), token, map[string]any{"page_segment": "Strona!"}), "page_segment", "Use lowercase letters, digits and hyphens only")
|
||||
refusal(t, env.call(t, http.MethodPut, adminAPI("/golem15/seo/settings/1"), token, map[string]any{"page_title_suffix": "– %foo%"}), "page_title_suffix", "Unknown placeholder “%foo%”")
|
||||
refusal(t, env.call(t, http.MethodPut, "/_seo/api/v1/settings", token, map[string]any{"page_form": "hash"}), "page_form", "")
|
||||
})
|
||||
}
|
||||
|
||||
// TestPagedAlternates: the hreflang alternates of page 2 and later point
|
||||
// at the same page of each translation.
|
||||
func TestPagedAlternates(t *testing.T) {
|
||||
env := bootBlog(t)
|
||||
resetBlog()
|
||||
res := env.resolve(t, "/blog/first-post?page=2")
|
||||
want := `[{"hreflang":"en","url":"https://www.example.test/blog/first-post?page=2"},{"hreflang":"pl","url":"https://www.example.test/pl/blog/pierwszy-wpis?page=2"},{"hreflang":"x-default","url":"https://www.example.test/blog/first-post?page=2"}]`
|
||||
if got := asJSON(t, res["alternates"]); got != want {
|
||||
t.Fatalf("paged alternates = %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user