Files
sm-seo-plugin/classes/alternates.go
Jakub Zych ea2ce24954 feat: JSON-LD graph, breadcrumbs and hreflang in the resolve answer (tracer)
- classes/schema builds one @graph per page (WebPage, article, profile,
  service, BreadcrumbList, VideoObject, WebSite, publisher) linked by @id
- breadcrumb trail through the primary parent chain, hreflang alternates
  with x-default, html_lang and og:locale in the resolve answer
- migration 202610280004 adds the publisher, local business, home crumb
  and pagination settings columns
2026-11-04 14:06:57 +01:00

120 lines
3.5 KiB
Go

package classes
import (
"strings"
"git.golem15.com/golem15/summercms/modules/pact"
)
// OGLocalesConfigKey maps locale codes to og:locale values (D-55); unset
// codes fall back to DefaultOGLocales, else <code>_<CODE>.
const OGLocalesConfigKey = "golem15.seo.og_locales"
// SearchURLConfigKey is the site search address with the
// {search_term_string} placeholder (P33-25); empty means no SearchAction.
const SearchURLConfigKey = "golem15.seo.search_url"
// DefaultOGLocales are the og:locale values of the common locales.
var DefaultOGLocales = map[string]string{"pl": "pl_PL", "en": "en_US", "de": "de_DE"}
// Alternate is one hreflang alternate of a page (D-55): a language tag or
// x-default and the absolute URL.
type Alternate struct {
Hreflang string `json:"hreflang"`
URL string `json:"url"`
}
// LangTag turns a locale code into its language tag: pl stays pl, pt_BR
// and pt-br become pt-BR (the region is kept when the locale has one).
func LangTag(locale string) string {
locale = strings.TrimSpace(strings.ReplaceAll(locale, "_", "-"))
lang, region, ok := strings.Cut(locale, "-")
if !ok || region == "" {
return strings.ToLower(lang)
}
return strings.ToLower(lang) + "-" + strings.ToUpper(region)
}
// OGLocale is the og:locale value of locale: the configured value, else
// the default table, else the language with its region (pt_BR) or the
// language doubled (fr_FR).
func (s *Service) OGLocale(locale string) string {
tag := LangTag(locale)
if tag == "" {
return ""
}
if cfg := s.Config(); cfg != nil {
for _, key := range []string{locale, tag, strings.ToLower(tag)} {
if v := strings.TrimSpace(cfg.String(OGLocalesConfigKey + "." + key)); v != "" {
return v
}
}
}
if v, ok := DefaultOGLocales[tag]; ok {
return v
}
lang, region, ok := strings.Cut(tag, "-")
if ok {
return lang + "_" + region
}
return lang + "_" + strings.ToUpper(lang)
}
// translatedLocales lists the locales of rec with a public address, in the
// record's order.
func translatedLocales(rec *pact.ContentRecord) []pact.ContentLocale {
if rec == nil {
return nil
}
var out []pact.ContentLocale
seen := map[string]bool{}
for _, l := range rec.Locales {
if strings.TrimSpace(l.URL) == "" || l.Locale == "" || seen[l.Locale] {
continue
}
seen[l.Locale] = true
out = append(out, l)
}
return out
}
// Alternates lists the hreflang alternates of a record (D-55 to D-57):
// each translated locale with its absolute URL, the page itself included,
// then x-default on the default locale's URL. A record translated into one
// locale only has none; a locale without a translation is absent.
func Alternates(site string, rec *pact.ContentRecord, defaultLocale string) []Alternate {
locales := translatedLocales(rec)
if len(locales) < 2 {
return []Alternate{}
}
out := make([]Alternate, 0, len(locales)+1)
xDefault := ""
for _, l := range locales {
u := AbsoluteURL(site, l.URL)
out = append(out, Alternate{Hreflang: LangTag(l.Locale), URL: u})
if l.Locale == defaultLocale {
xDefault = u
}
}
if xDefault != "" {
out = append(out, Alternate{Hreflang: "x-default", URL: xDefault})
}
return out
}
// OGLocaleAlternates are the og:locale:alternate values of a record: one
// per other translated locale.
func (s *Service) OGLocaleAlternates(rec *pact.ContentRecord, locale string) []string {
out := []string{}
locales := translatedLocales(rec)
if len(locales) < 2 {
return out
}
for _, l := range locales {
if l.Locale != locale {
out = append(out, s.OGLocale(l.Locale))
}
}
return out
}