148 lines
4.2 KiB
Go
148 lines
4.2 KiB
Go
package seo
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/signpost"
|
|
)
|
|
|
|
// TabSEO is the key of the SEO tab added to every content type's form.
|
|
const TabSEO = "golem15.seo::lang.tab.seo"
|
|
|
|
// AdminActionExtensions adds the SEO tab (D-01, D-91) to the admin form of
|
|
// every content type registered in signpost with a content descriptor that
|
|
// names a controller. The content plugins import nothing from this plugin.
|
|
func (p *Plugin) AdminActionExtensions() []pact.AdminActionExtension {
|
|
reg := p.registry()
|
|
if reg == nil {
|
|
return nil
|
|
}
|
|
var out []pact.AdminActionExtension
|
|
for _, typ := range reg.ContentTypes() {
|
|
if typ.Content == nil || strings.TrimSpace(typ.Content.Controller) == "" {
|
|
continue
|
|
}
|
|
out = append(out, pact.AdminActionExtension{
|
|
Controller: typ.Content.Controller,
|
|
Form: p.seoTab(typ.Key),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (p *Plugin) registry() *signpost.Registry {
|
|
if p == nil || p.app == nil {
|
|
return nil
|
|
}
|
|
reg, _ := p.app.Lookup[*signpost.Registry]()
|
|
return reg
|
|
}
|
|
|
|
// seoTabYAML is the SEO tab's fields document: the tab placed after the
|
|
// owner's tabs (no anchor) and its fields.
|
|
const seoTabYAML = `tabs:
|
|
- name: golem15.seo::lang.tab.seo
|
|
fields:
|
|
seo_title:
|
|
label: golem15.seo::lang.field.title
|
|
type: mltext
|
|
tab: golem15.seo::lang.tab.seo
|
|
`
|
|
|
|
// seoTab is the form extension of content type key.
|
|
func (p *Plugin) seoTab(key string) *pact.AdminFormExtension {
|
|
return &pact.AdminFormExtension{
|
|
FieldsYAML: []byte(seoTabYAML),
|
|
Load: func(ctx context.Context, in pact.AdminFormLoad) (map[string]any, error) {
|
|
return p.loadSEOTab(ctx, key, in)
|
|
},
|
|
Save: func(ctx context.Context, in pact.AdminFormSave) (pact.AdminFormSaveResult, error) {
|
|
return p.saveSEOTab(ctx, key, in)
|
|
},
|
|
}
|
|
}
|
|
|
|
// contentType returns the registered content type key with a descriptor.
|
|
func (p *Plugin) contentType(key string) (pact.MenuItemType, error) {
|
|
reg := p.registry()
|
|
if reg == nil {
|
|
return pact.MenuItemType{}, fmt.Errorf("golem15.seo: menu item type registry is not published")
|
|
}
|
|
typ, ok := reg.ContentType(key)
|
|
if !ok {
|
|
return pact.MenuItemType{}, fmt.Errorf("golem15.seo: content type %q is not registered", key)
|
|
}
|
|
return typ, nil
|
|
}
|
|
|
|
// loadSEOTab returns the stored SEO tab values of a record, every field a
|
|
// map of locale to text.
|
|
func (p *Plugin) loadSEOTab(ctx context.Context, key string, in pact.AdminFormLoad) (map[string]any, error) {
|
|
svc := p.Service()
|
|
if svc == nil {
|
|
return nil, classes.ErrNotBooted
|
|
}
|
|
typ, err := p.contentType(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries, err := classes.LoadEntries(in.DB.WithContext(ctx), typ.Content.Model, in.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]any{}
|
|
for column, perLocale := range classes.RecordValuesOf(entries) {
|
|
out[classes.FieldPrefix+column] = perLocale
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// saveSEOTab stores the submitted SEO tab values of a record inside the
|
|
// record's save transaction.
|
|
func (p *Plugin) saveSEOTab(ctx context.Context, key string, in pact.AdminFormSave) (pact.AdminFormSaveResult, error) {
|
|
svc := p.Service()
|
|
if svc == nil {
|
|
return pact.AdminFormSaveResult{}, classes.ErrNotBooted
|
|
}
|
|
typ, err := p.contentType(key)
|
|
if err != nil {
|
|
return pact.AdminFormSaveResult{}, err
|
|
}
|
|
values := classes.RecordValues{}
|
|
for _, column := range classes.EntryColumns {
|
|
raw, ok := in.Values[classes.FieldPrefix+column]
|
|
if !ok {
|
|
continue
|
|
}
|
|
values[column] = localeMap(raw)
|
|
}
|
|
if _, _, err := svc.SaveRecord(ctx, in.DB, classes.RecordWrite{Type: typ, ID: in.ID, Values: values}); err != nil {
|
|
return pact.AdminFormSaveResult{}, err
|
|
}
|
|
return pact.AdminFormSaveResult{}, nil
|
|
}
|
|
|
|
// localeMap reads a submitted value as locale -> text: a translatable
|
|
// field's map, or a plain string under the empty locale key.
|
|
func localeMap(raw any) map[string]string {
|
|
switch v := raw.(type) {
|
|
case map[string]string:
|
|
return v
|
|
case map[string]any:
|
|
out := make(map[string]string, len(v))
|
|
for locale, value := range v {
|
|
if s, ok := value.(string); ok {
|
|
out[locale] = s
|
|
}
|
|
}
|
|
return out
|
|
case string:
|
|
return map[string]string{"": v}
|
|
}
|
|
return map[string]string{}
|
|
}
|