feat: SEO field types and the complete SEO tab
This commit is contained in:
131
extensions.go
131
extensions.go
@@ -31,7 +31,7 @@ func (p *Plugin) AdminActionExtensions() []pact.AdminActionExtension {
|
||||
}
|
||||
out = append(out, pact.AdminActionExtension{
|
||||
Controller: typ.Content.Controller,
|
||||
Form: p.seoTab(typ.Key),
|
||||
Form: p.seoTab(typ.Key, typ.Label),
|
||||
})
|
||||
}
|
||||
if form := p.typeSettingsTabs(types); form != nil {
|
||||
@@ -76,25 +76,101 @@ func (p *Plugin) registry() *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.
|
||||
// seoTabYAML is the SEO tab's fields document (UI-SPEC C3): the tab placed
|
||||
// after the owner's tabs (no anchor, P-17) and its fields in order. The
|
||||
// names seo_robots and seo_og_image are anchors Phase 33 places fields
|
||||
// after; they must not be renamed.
|
||||
const seoTabYAML = `tabs:
|
||||
- name: golem15.seo::lang.tab.seo
|
||||
fields:
|
||||
seo_status:
|
||||
type: partial
|
||||
path: seo_status
|
||||
span: full
|
||||
context: update
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
seo_snippet:
|
||||
label: golem15.seo::lang.field.snippet
|
||||
type: widget
|
||||
widget: golem15-seo-serp
|
||||
action: seo_snippet
|
||||
fill: [seo_title, seo_description, seo_keyphrase]
|
||||
span: full
|
||||
context: [create, update]
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
seo_title:
|
||||
label: golem15.seo::lang.field.title
|
||||
comment: golem15.seo::lang.field.title_comment
|
||||
type: mlseotext
|
||||
measure: title
|
||||
span: full
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
seo_description:
|
||||
label: golem15.seo::lang.field.description
|
||||
comment: golem15.seo::lang.field.description_comment
|
||||
type: mlseotext
|
||||
measure: description
|
||||
span: full
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
seo_keyphrase:
|
||||
label: golem15.seo::lang.field.keyphrase
|
||||
comment: golem15.seo::lang.field.keyphrase_comment
|
||||
type: mltext
|
||||
span: left
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
seo_canonical:
|
||||
label: golem15.seo::lang.field.canonical
|
||||
comment: golem15.seo::lang.field.canonical_comment
|
||||
type: mltext
|
||||
span: right
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
seo_robots:
|
||||
label: golem15.seo::lang.field.robots
|
||||
comment: golem15.seo::lang.field.robots_comment
|
||||
type: mlseorobots
|
||||
scope: record
|
||||
span: full
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
seo_og_title:
|
||||
label: golem15.seo::lang.field.og_title
|
||||
comment: golem15.seo::lang.field.og_title_comment
|
||||
type: mltext
|
||||
span: full
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
seo_og_description:
|
||||
label: golem15.seo::lang.field.og_description
|
||||
type: mltextarea
|
||||
size: small
|
||||
span: full
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
seo_og_image:
|
||||
label: golem15.seo::lang.field.og_image
|
||||
comment: golem15.seo::lang.field.og_image_comment
|
||||
type: seoimage
|
||||
span: full
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
seo_effective:
|
||||
label: golem15.seo::lang.field.effective
|
||||
type: partial
|
||||
path: seo_effective
|
||||
span: full
|
||||
context: update
|
||||
tab: golem15.seo::lang.tab.seo
|
||||
`
|
||||
|
||||
// seoTab is the form extension of content type key.
|
||||
func (p *Plugin) seoTab(key string) *pact.AdminFormExtension {
|
||||
func (p *Plugin) seoTab(key, label string) *pact.AdminFormExtension {
|
||||
return &pact.AdminFormExtension{
|
||||
FieldsYAML: []byte(seoTabYAML),
|
||||
Permissions: []string{controllers.PermissionManage},
|
||||
Visible: func(ctx context.Context) bool {
|
||||
return p.injected(ctx, key)
|
||||
},
|
||||
Dir: "extension",
|
||||
Actions: []pact.AdminAction{classes.RecordSnippetAction(p.Service, key, "golem15.seo::lang.snippet.action", controllers.PermissionManage)},
|
||||
PartialData: func(ctx context.Context, name string, record any) (any, error) {
|
||||
return p.seoTabPartial(ctx, key, name, record)
|
||||
},
|
||||
Load: func(ctx context.Context, in pact.AdminFormLoad) (map[string]any, error) {
|
||||
return p.loadSEOTab(ctx, key, in)
|
||||
},
|
||||
@@ -104,6 +180,32 @@ func (p *Plugin) seoTab(key string) *pact.AdminFormExtension {
|
||||
}
|
||||
}
|
||||
|
||||
// seoTabPartial serves P1 seo_status and P2 seo_effective of a record.
|
||||
func (p *Plugin) seoTabPartial(ctx context.Context, key, name string, record any) (any, error) {
|
||||
svc := p.Service()
|
||||
if svc == nil {
|
||||
return nil, classes.ErrNotBooted
|
||||
}
|
||||
db, err := svc.DB(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := classes.RecordKey(db, record)
|
||||
if err != nil {
|
||||
if name == "seo_effective" {
|
||||
return classes.EffectiveView{}, nil
|
||||
}
|
||||
return classes.StatusView{}, nil
|
||||
}
|
||||
switch name {
|
||||
case "seo_status":
|
||||
return svc.RecordStatus(ctx, key, id)
|
||||
case "seo_effective":
|
||||
return svc.RecordEffective(ctx, key, id)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// contentType returns the registered content type key with a descriptor.
|
||||
func (p *Plugin) contentType(key string) (pact.MenuItemType, error) {
|
||||
reg := p.registry()
|
||||
@@ -134,6 +236,10 @@ func (p *Plugin) loadSEOTab(ctx context.Context, key string, in pact.AdminFormLo
|
||||
}
|
||||
out := map[string]any{}
|
||||
for column, perLocale := range classes.RecordValuesOf(entries) {
|
||||
if column == classes.SharedColumn {
|
||||
out[classes.FieldPrefix+column] = classes.SharedValue(entries)
|
||||
continue
|
||||
}
|
||||
out[classes.FieldPrefix+column] = perLocale
|
||||
}
|
||||
return out, nil
|
||||
@@ -151,12 +257,27 @@ func (p *Plugin) saveSEOTab(ctx context.Context, key string, in pact.AdminFormSa
|
||||
return pact.AdminFormSaveResult{}, err
|
||||
}
|
||||
values := classes.RecordValues{}
|
||||
refusals := pact.AdminFormErrors{}
|
||||
for _, column := range classes.EntryColumns {
|
||||
raw, ok := in.Values[classes.FieldPrefix+column]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
values[column] = localeMap(raw)
|
||||
perLocale := localeMap(raw)
|
||||
if column == "canonical" {
|
||||
for _, v := range perLocale {
|
||||
if v = strings.TrimSpace(v); v != "" && !classes.ValidURLRef(v) {
|
||||
refusals[classes.FieldPrefix+column] = []string{svc.PhraseCtx(ctx, "lagoon::validation.url", map[string]string{
|
||||
"attribute": svc.PhraseCtx(ctx, "golem15.seo::lang.field.canonical", nil),
|
||||
})}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
values[column] = perLocale
|
||||
}
|
||||
if len(refusals) > 0 {
|
||||
return pact.AdminFormSaveResult{}, refusals
|
||||
}
|
||||
if _, _, err := svc.SaveRecord(ctx, in.DB, classes.RecordWrite{Type: typ, ID: in.ID, Values: values}); err != nil {
|
||||
return pact.AdminFormSaveResult{}, err
|
||||
|
||||
Reference in New Issue
Block a user