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

@@ -44,45 +44,50 @@ var SettingsColumns = []string{
"publisher_email", "publisher_phone", "publisher_same_as",
"local_enabled", "local_type", "local_price_range", "local_street",
"local_postal_code", "local_city", "local_region", "local_country",
"local_latitude", "local_longitude", "local_hours",
"local_latitude", "local_longitude", "local_hours", "page_form",
"page_segment", "page_title_suffix", "page_description_suffix",
}
// settingsText maps the plain text settings columns to their fields.
func settingsText(st *models.Settings) map[string]*string {
return map[string]*string{
"site_name": &st.SiteName,
"twitter_site": &st.TwitterSite,
"title_template": &st.TitleTemplate,
"home_template": &st.HomeTemplate,
"default_description": &st.DefaultDescription,
"default_og_image": &st.DefaultOGImage,
"noindex_paths": &st.NoindexPaths,
"search_paths": &st.SearchPaths,
"url_host": &st.URLHost,
"url_slash": &st.URLSlash,
"verify_google": &st.VerifyGoogle,
"verify_bing": &st.VerifyBing,
"verify_yandex": &st.VerifyYandex,
"csv_delimiter": &st.CSVDelimiter,
"nofollow_domains": &st.NofollowDomains,
"sponsored_domains": &st.SponsoredDomains,
"breadcrumb_home": &st.BreadcrumbHome,
"publisher_type": &st.PublisherType,
"publisher_name": &st.PublisherName,
"publisher_logo": &st.PublisherLogo,
"publisher_url": &st.PublisherURL,
"publisher_email": &st.PublisherEmail,
"publisher_phone": &st.PublisherPhone,
"publisher_same_as": &st.PublisherSameAs,
"local_type": &st.LocalType,
"local_price_range": &st.LocalPriceRange,
"local_street": &st.LocalStreet,
"local_postal_code": &st.LocalPostalCode,
"local_city": &st.LocalCity,
"local_region": &st.LocalRegion,
"local_country": &st.LocalCountry,
"local_latitude": &st.LocalLatitude,
"local_longitude": &st.LocalLongitude,
"site_name": &st.SiteName,
"twitter_site": &st.TwitterSite,
"title_template": &st.TitleTemplate,
"home_template": &st.HomeTemplate,
"default_description": &st.DefaultDescription,
"default_og_image": &st.DefaultOGImage,
"noindex_paths": &st.NoindexPaths,
"search_paths": &st.SearchPaths,
"url_host": &st.URLHost,
"url_slash": &st.URLSlash,
"verify_google": &st.VerifyGoogle,
"verify_bing": &st.VerifyBing,
"verify_yandex": &st.VerifyYandex,
"csv_delimiter": &st.CSVDelimiter,
"nofollow_domains": &st.NofollowDomains,
"sponsored_domains": &st.SponsoredDomains,
"breadcrumb_home": &st.BreadcrumbHome,
"publisher_type": &st.PublisherType,
"publisher_name": &st.PublisherName,
"publisher_logo": &st.PublisherLogo,
"publisher_url": &st.PublisherURL,
"publisher_email": &st.PublisherEmail,
"publisher_phone": &st.PublisherPhone,
"publisher_same_as": &st.PublisherSameAs,
"local_type": &st.LocalType,
"local_price_range": &st.LocalPriceRange,
"local_street": &st.LocalStreet,
"local_postal_code": &st.LocalPostalCode,
"local_city": &st.LocalCity,
"local_region": &st.LocalRegion,
"local_country": &st.LocalCountry,
"local_latitude": &st.LocalLatitude,
"local_longitude": &st.LocalLongitude,
"page_form": &st.PageForm,
"page_segment": &st.PageSegment,
"page_title_suffix": &st.PageTitleSuffix,
"page_description_suffix": &st.PageDescriptionSuffix,
}
}
@@ -90,46 +95,50 @@ func settingsText(st *models.Settings) map[string]*string {
// history and the API.
func SettingsValues(st models.Settings) map[string]any {
return map[string]any{
"site_name": st.SiteName,
"twitter_site": st.TwitterSite,
"title_template": st.TitleTemplate,
"home_template": st.HomeTemplate,
"default_description": st.DefaultDescription,
"default_og_image": st.DefaultOGImage,
"query_params": plainValue(st.Params()),
"noindex_paths": st.NoindexPaths,
"search_paths": st.SearchPaths,
"url_host": st.URLHost,
"url_slash": st.URLSlash,
"url_lowercase": st.URLLowercase,
"links_new_tab": st.LinksNewTab,
"nofollow_domains": st.NofollowDomains,
"sponsored_domains": st.SponsoredDomains,
"verify_google": st.VerifyGoogle,
"verify_bing": st.VerifyBing,
"verify_yandex": st.VerifyYandex,
"verify_other": plainValue(st.OtherMeta()),
"verify_files": plainValue(st.Files()),
"csv_delimiter": st.CSVDelimiter,
"breadcrumb_home": st.BreadcrumbHome,
"publisher_type": st.PublisherType,
"publisher_name": st.PublisherName,
"publisher_logo": st.PublisherLogo,
"publisher_url": st.PublisherURL,
"publisher_email": st.PublisherEmail,
"publisher_phone": st.PublisherPhone,
"publisher_same_as": st.PublisherSameAs,
"local_enabled": st.LocalEnabled,
"local_type": st.LocalType,
"local_price_range": st.LocalPriceRange,
"local_street": st.LocalStreet,
"local_postal_code": st.LocalPostalCode,
"local_city": st.LocalCity,
"local_region": st.LocalRegion,
"local_country": st.LocalCountry,
"local_latitude": st.LocalLatitude,
"local_longitude": st.LocalLongitude,
"local_hours": plainValue(st.Hours()),
"site_name": st.SiteName,
"twitter_site": st.TwitterSite,
"title_template": st.TitleTemplate,
"home_template": st.HomeTemplate,
"default_description": st.DefaultDescription,
"default_og_image": st.DefaultOGImage,
"query_params": plainValue(st.Params()),
"noindex_paths": st.NoindexPaths,
"search_paths": st.SearchPaths,
"url_host": st.URLHost,
"url_slash": st.URLSlash,
"url_lowercase": st.URLLowercase,
"links_new_tab": st.LinksNewTab,
"nofollow_domains": st.NofollowDomains,
"sponsored_domains": st.SponsoredDomains,
"verify_google": st.VerifyGoogle,
"verify_bing": st.VerifyBing,
"verify_yandex": st.VerifyYandex,
"verify_other": plainValue(st.OtherMeta()),
"verify_files": plainValue(st.Files()),
"csv_delimiter": st.CSVDelimiter,
"breadcrumb_home": st.BreadcrumbHome,
"publisher_type": st.PublisherType,
"publisher_name": st.PublisherName,
"publisher_logo": st.PublisherLogo,
"publisher_url": st.PublisherURL,
"publisher_email": st.PublisherEmail,
"publisher_phone": st.PublisherPhone,
"publisher_same_as": st.PublisherSameAs,
"local_enabled": st.LocalEnabled,
"local_type": st.LocalType,
"local_price_range": st.LocalPriceRange,
"local_street": st.LocalStreet,
"local_postal_code": st.LocalPostalCode,
"local_city": st.LocalCity,
"local_region": st.LocalRegion,
"local_country": st.LocalCountry,
"local_latitude": st.LocalLatitude,
"local_longitude": st.LocalLongitude,
"local_hours": plainValue(st.Hours()),
"page_form": st.PageForm,
"page_segment": st.PageSegment,
"page_title_suffix": st.PageTitleSuffix,
"page_description_suffix": st.PageDescriptionSuffix,
}
}