package seo import ( "context" "fmt" "strings" "git.golem15.com/golem15/sm-seo-plugin/classes" "git.golem15.com/golem15/sm-seo-plugin/controllers" "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 types := reg.ContentTypes() for _, typ := range types { if typ.Content == nil || strings.TrimSpace(typ.Content.Controller) == "" { continue } out = append(out, pact.AdminActionExtension{ Controller: typ.Content.Controller, Form: p.seoTab(typ.Key), }) } if form := p.typeSettingsTabs(types); form != nil { out = append(out, pact.AdminActionExtension{Controller: controllers.SettingsControllerID, Form: form}) } 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), Permissions: []string{controllers.PermissionManage}, Visible: func(ctx context.Context) bool { return p.injected(ctx, key) }, 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{} } // injected reports whether the SEO tab of content type key is shown: false // only when its settings row turns it off. It answers true when it cannot // decide, so a database error never hides stored values. func (p *Plugin) injected(ctx context.Context, key string) bool { svc := p.Service() if svc == nil { return true } db, err := svc.DB(ctx) if err != nil { return true } row, err := classes.LoadTypeSetting(db, key) if err != nil { return true } return row.Inject }