package classes import ( "context" "strings" "git.golem15.com/golem15/sm-seo-plugin/classes/schema" "git.golem15.com/golem15/sm-seo-plugin/models" "git.golem15.com/golem15/summercms/modules/pact" "gorm.io/gorm" ) // The entry data keys of the structured data fields (P33-23): one value per // record, written on every locale row as the social image is. const ( DataPrimaryParent = "primary_parent" DataSchema = "schema" ) // MaxCrumbDepth caps the parent chain of a breadcrumb trail (T-33-28). const MaxCrumbDepth = 6 // ParentValue is the stored form of a parent reference: :. func ParentValue(ref pact.ContentRef) string { return ref.Type + ":" + ref.ID } // entryData reads a string value of an entry's data. func entryData(e models.Entry, key string) string { if e.Data == nil { return "" } s, _ := e.Data[key].(string) return strings.TrimSpace(s) } // PrimaryParent is a record's primary parent (D-86): the stored choice // when the record still has that parent, else its first parent. gone is // true when a stored choice is no longer one of the parents; ok is false // when the record has no parent. func PrimaryParent(rec *pact.ContentRecord, stored string) (ref pact.ContentRef, ok, gone bool) { if rec == nil || len(rec.Parents) == 0 { return pact.ContentRef{}, false, stored != "" } if stored != "" { for _, p := range rec.Parents { if ParentValue(p) == stored { return p, true, false } } gone = true } return rec.Parents[0], true, gone } // localeOf returns the record's description in locale exactly; ok is // false when it has none with an address. func localeOf(rec *pact.ContentRecord, locale string) (pact.ContentLocale, bool) { if rec == nil { return pact.ContentLocale{}, false } for _, l := range rec.Locales { if l.Locale == locale && strings.TrimSpace(l.URL) != "" { return l, true } } return pact.ContentLocale{}, false } // HomeCrumbName is the home crumb's name in locale: the setting, else // the default copy. func (s *Service) HomeCrumbName(ctx context.Context, db *gorm.DB, settings models.Settings, locale string) string { name := strings.TrimSpace(s.translatedSetting(ctx, db, &settings, "breadcrumb_home", settings.BreadcrumbHome, locale)) if name == "" { name = s.PhraseIn(locale, "golem15.seo::lang.settings.breadcrumb_home_default", nil) } return name } // homePath is the home page of a locale's address: / when the // page's path carries that prefix, else /. func homePath(locale, path string) string { if locale != "" { prefix := "/" + strings.ToLower(locale) if lower := strings.ToLower(path); lower == prefix || strings.HasPrefix(lower, prefix+"/") { return prefix } } return "/" } // parentChain lists a record's parents for its trail, the nearest last: // the primary parent (the stored choice, else the first parent), then that // parent's primary parent, at most MaxCrumbDepth levels. An unpublished // parent or one without an address in locale is left out of the trail but // its own parent is still followed; a cycle ends the chain. func (s *Service) parentChain(ctx context.Context, db *gorm.DB, rec *pact.ContentRecord, stored, locale string) []parentCrumb { reg := s.Registry() if reg == nil || rec == nil { return nil } var out []parentCrumb seen := map[string]bool{} current, choice := rec, stored for depth := 0; depth < MaxCrumbDepth; depth++ { ref, ok, _ := PrimaryParent(current, choice) if !ok || seen[ParentValue(ref)] { break } seen[ParentValue(ref)] = true parent, err := reg.Describe(ctx, ref.Type, ref.ID, s.UnprefixedLocale()) if err != nil || parent == nil { break } if l, has := localeOf(parent, locale); has && parent.Published { out = append(out, parentCrumb{ref: ref, locale: l}) } choice = "" if typ, ok := reg.ContentType(ref.Type); ok && typ.Content != nil { if e, found, err := LoadEntry(db, typ.Content.Model, ref.ID, locale); err == nil && found { choice = entryData(e, DataPrimaryParent) } } current = parent } for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 { out[i], out[j] = out[j], out[i] } return out } // parentCrumb is one parent of a trail. type parentCrumb struct { ref pact.ContentRef locale pact.ContentLocale } // Trail is a page's breadcrumb trail (D-86): the home crumb, the record's // parent chain and the page itself, last. The home page's trail is the // home crumb only. The breadcrumbs of the resolve answer and the // BreadcrumbList node are both this list. func (s *Service) Trail(ctx context.Context, db *gorm.DB, settings models.Settings, rec *pact.ContentRecord, stored, locale, path, name string) []schema.Crumb { site := s.SiteURL() home := homePath(locale, path) homeURL := site + home if home == "/" { homeURL = site + "/" } out := []schema.Crumb{{Name: s.HomeCrumbName(ctx, db, settings, locale), URL: homeURL, Path: home}} if path == "/" || path == home { return out } for _, p := range s.parentChain(ctx, db, rec, stored, locale) { title := strings.TrimSpace(p.locale.Title) if title == "" { continue } out = append(out, schema.Crumb{Name: title, URL: AbsoluteURL(site, p.locale.URL), Path: p.locale.URL}) } if strings.TrimSpace(name) == "" { name = path } return append(out, schema.Crumb{Name: name, URL: site + path, Path: path}) } // ParentOption is one choice of the primary category field (P33-23). type ParentOption struct { Value string `json:"value"` Label string `json:"label"` } // ParentOptions lists a record's parents as primary category choices, in // descriptor order, labelled with their title in locale (the default // locale when ""). func (s *Service) ParentOptions(ctx context.Context, key, id, locale string) []ParentOption { out := []ParentOption{} reg := s.Registry() if reg == nil || key == "" || id == "" { return out } rec, err := reg.Describe(ctx, key, id, s.UnprefixedLocale()) if err != nil || rec == nil { return out } if locale == "" { if db, err := s.DB(ctx); err == nil { locale = s.DefaultLocale(ctx, db) } } for _, ref := range rec.Parents { label := ParentValue(ref) if parent, err := reg.Describe(ctx, ref.Type, ref.ID, s.UnprefixedLocale()); err == nil && parent != nil { if l := contentLocale(parent, locale); strings.TrimSpace(l.Title) != "" { label = l.Title } } out = append(out, ParentOption{Value: ParentValue(ref), Label: label}) } return out }