- 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
167 lines
5.3 KiB
Go
167 lines
5.3 KiB
Go
package classes
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/sm-seo-plugin/models"
|
|
"git.golem15.com/golem15/sm-sitemap-plugin/contract"
|
|
)
|
|
|
|
// SiteAnswer is the answer of GET /_seo/api/v1/site (P33-17): the facts
|
|
// about the whole site the frontend layer needs, all public. SiteURL,
|
|
// DefaultLocale, Locales and UnprefixedLocale are the resolver's; Links
|
|
// the settings' new-tab switch; HTMLSitemap the sitemap plugin's HTML
|
|
// 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; 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"`
|
|
Locales []string `json:"locales"`
|
|
UnprefixedLocale string `json:"unprefixed_locale"`
|
|
Links SiteLinks `json:"links"`
|
|
HTMLSitemap SiteHTMLSitemap `json:"html_sitemap"`
|
|
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.
|
|
type SiteLinks struct {
|
|
NewTab bool `json:"new_tab"`
|
|
}
|
|
|
|
// SiteHTMLSitemap is the site answer's HTML sitemap block: whether the
|
|
// sitemap plugin serves it, its path per locale and its page size.
|
|
type SiteHTMLSitemap struct {
|
|
Enabled bool `json:"enabled"`
|
|
Paths map[string]string `json:"paths"`
|
|
PageSize int `json:"page_size"`
|
|
}
|
|
|
|
// SiteRoute is one content type's URL pattern.
|
|
type SiteRoute struct {
|
|
Type string `json:"type"`
|
|
Pattern string `json:"pattern"`
|
|
}
|
|
|
|
// listing returns the sitemap plugin's published contract.Listing, nil
|
|
// when none is published.
|
|
func (s *Service) listing() contract.Listing {
|
|
if s == nil || s.app == nil {
|
|
return nil
|
|
}
|
|
l, ok := s.app.Lookup[contract.Listing]()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return l
|
|
}
|
|
|
|
// Site builds the site answer. It names only public facts: no token, no
|
|
// unpublished data (T-33-19).
|
|
func (s *Service) Site(ctx context.Context) (SiteAnswer, error) {
|
|
out := SiteAnswer{
|
|
SiteURL: s.SiteURL(),
|
|
UnprefixedLocale: s.UnprefixedLocale(),
|
|
HTMLSitemap: SiteHTMLSitemap{Paths: map[string]string{}},
|
|
Routes: []SiteRoute{},
|
|
RootFiles: []string{},
|
|
}
|
|
db, err := s.DB(ctx)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
settings, err := LoadSettings(db)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
robots, err := LoadRobots(db)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
out.DefaultLocale = s.DefaultLocale(ctx, db)
|
|
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 {
|
|
continue
|
|
}
|
|
out.Routes = append(out.Routes, SiteRoute{Type: typ.Key, Pattern: reg.Pattern(typ.Key, "")})
|
|
}
|
|
}
|
|
seen := map[string]bool{}
|
|
addFile := func(name string) {
|
|
name = strings.TrimSpace(name)
|
|
if name != "" && verificationName.MatchString(name) && !seen[name] {
|
|
seen[name] = true
|
|
out.RootFiles = append(out.RootFiles, name)
|
|
}
|
|
}
|
|
for _, f := range settings.Files() {
|
|
addFile(f.Filename)
|
|
}
|
|
if l := s.listing(); l != nil {
|
|
st, err := l.Settings(ctx)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
out.HTMLSitemap.Enabled = st.HTMLEnabled
|
|
for locale, path := range st.HTMLPaths {
|
|
if strings.TrimSpace(path) != "" {
|
|
out.HTMLSitemap.Paths[locale] = path
|
|
}
|
|
}
|
|
out.HTMLSitemap.PageSize = st.HTMLPageSize
|
|
addFile(st.KeyFile)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// SiteName is the site's name in locale: the settings' site name, else
|
|
// the application name.
|
|
func (s *Service) SiteName(ctx context.Context, settings models.Settings, locale string) string {
|
|
db, err := s.DB(ctx)
|
|
name := settings.SiteName
|
|
if err == nil {
|
|
name = s.translatedSetting(ctx, db, &settings, "site_name", settings.SiteName, locale)
|
|
}
|
|
if strings.TrimSpace(name) == "" {
|
|
if cfg := s.Config(); cfg != nil {
|
|
name = cfg.String("app.name")
|
|
}
|
|
}
|
|
return strings.TrimSpace(name)
|
|
}
|