feat: SEO field types and the complete SEO tab

This commit is contained in:
2026-10-27 11:04:03 +01:00
parent deebd9aa65
commit 93ddbcc9ef
26 changed files with 1827 additions and 50 deletions

View File

@@ -3,6 +3,7 @@ package classes
import (
"context"
"fmt"
"sort"
"strings"
"git.golem15.com/golem15/sm-seo-plugin/models"
@@ -17,6 +18,26 @@ var EntryColumns = []string{
"og_title", "og_description", "og_image",
}
// SharedColumn is the column whose value is one per record, shared by
// every locale row (Q-5): the social image.
const SharedColumn = "og_image"
// SharedValue returns a record's shared social image: the first non-empty
// value in locale order.
func SharedValue(entries map[string]models.Entry) string {
locales := make([]string, 0, len(entries))
for locale := range entries {
locales = append(locales, locale)
}
sort.Strings(locales)
for _, locale := range locales {
if v := entries[locale].OGImage; v != "" {
return v
}
}
return ""
}
// FieldPrefix is the prefix of the SEO tab's field names (seo_title, ...),
// so they never collide with an owner's fields.
const FieldPrefix = "seo_"
@@ -130,8 +151,29 @@ func (s *Service) SaveRecord(ctx context.Context, db *gorm.DB, in RecordWrite) (
before[locale] = e
}
described := s.recordLocales(ctx, in.Type.Key, in.ID)
values := in.Values
if shared, ok := values[SharedColumn]; ok {
// One value for every locale of the record (Q-5).
v := ""
for _, text := range shared {
v = text
}
all := map[string]string{}
for locale := range existing {
all[locale] = v
}
for locale := range described {
all[locale] = v
}
copied := RecordValues{}
for column, perLocale := range values {
copied[column] = perLocale
}
copied[SharedColumn] = all
values = copied
}
touched := map[string]models.Entry{}
for column, perLocale := range in.Values {
for column, perLocale := range values {
for locale, value := range perLocale {
e, ok := touched[locale]
if !ok {
@@ -156,6 +198,7 @@ func (s *Service) SaveRecord(ctx context.Context, db *gorm.DB, in RecordWrite) (
if l, ok := described[locale]; ok {
e.URL = l.URL
}
e.EffectiveTitle, e.EffectiveDescription = s.effectiveOf(ctx, db, in.Type.Key, e, described[locale], locale)
if e.ID == 0 {
if err := db.Create(&e).Error; err != nil {
return nil, nil, err
@@ -181,6 +224,21 @@ func (s *Service) SaveRecord(ctx context.Context, db *gorm.DB, in RecordWrite) (
return before, after, nil
}
// effectiveOf computes an entry's effective title and description cache.
func (s *Service) effectiveOf(ctx context.Context, db *gorm.DB, key string, e models.Entry, l pact.ContentLocale, locale string) (string, string) {
settings, err := LoadSettings(db)
if err != nil {
return e.EffectiveTitle, e.EffectiveDescription
}
ts, err := LoadTypeSetting(db, key)
if err != nil {
return e.EffectiveTitle, e.EffectiveDescription
}
site := s.translatedSetting(ctx, db, &settings, "site_name", settings.SiteName, locale)
fallback := s.translatedSetting(ctx, db, &settings, "default_description", settings.DefaultDescription, locale)
return EffectiveValues(e, l, settings, ts, site, fallback)
}
// recordWhat is the history description of a record's SEO in a locale:
// "SEO of <type> “<title>” (<locale>)".
func (s *Service) recordWhat(typ pact.MenuItemType, id string, l pact.ContentLocale, locale string) string {