feat: page registry with its admin screen and signpost type
- golem15_seo_pages (migration 202610280002): the site's own frontend pages
with an address and a name per locale, kind, origin, parent, content date
and note; settings routes_reported_at
- SEO > Pages (golem15.seo.pages) after URL rules: list with kind, origin
and missing filters and the missing row state, form with the P9 partial;
the plugin's form extension saves the address and name per locale and
refuses url_required, url_invalid, url_taken and url_owned
- menu item type seo-page with a content descriptor, so each page gets
the SEO tab, resolves ok and is listed in the sitemaps grouped by kind
(pages, offers, services, landing-pages, titled per locale)
- management API GET/POST /_seo/api/v1/pages, PUT/DELETE /pages/{id} and
the MCP tools seo_list_pages, seo_create_page, seo_update_page,
seo_delete_page (in this commit because the operations parity test maps
every Pages operation to its tool)
This commit is contained in:
142
extensions_pages.go
Normal file
142
extensions_pages.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package seo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-seo-plugin/controllers"
|
||||
"git.golem15.com/golem15/sm-seo-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// pagesFormYAML adds the translatable address and name to the Pages form
|
||||
// (UI-SPEC C21): url after page_status, title after kind.
|
||||
const pagesFormYAML = `fields:
|
||||
url:
|
||||
label: golem15.seo::lang.pages.url
|
||||
comment: golem15.seo::lang.pages.url_comment
|
||||
type: mltext
|
||||
span: left
|
||||
after: page_status
|
||||
title:
|
||||
label: golem15.seo::lang.pages.title
|
||||
comment: golem15.seo::lang.pages.title_comment
|
||||
type: mltext
|
||||
span: left
|
||||
after: kind
|
||||
`
|
||||
|
||||
// pagesForm is the plugin's form extension on its own Pages controller:
|
||||
// it loads and saves a page's address and name per locale (the default
|
||||
// locale in the url and title columns, the others in urls and titles),
|
||||
// refuses a page with the C21 copy (url_required, url_invalid, url_taken,
|
||||
// url_owned) inside the save transaction, and records the save in
|
||||
// history. It is listed before the SEO tab, so the tab's address tracking
|
||||
// sees the saved address.
|
||||
func (p *Plugin) pagesForm() *pact.AdminFormExtension {
|
||||
return &pact.AdminFormExtension{
|
||||
FieldsYAML: []byte(pagesFormYAML),
|
||||
Permissions: []string{controllers.PermissionManage},
|
||||
Load: func(ctx context.Context, in pact.AdminFormLoad) (map[string]any, error) {
|
||||
svc := p.Service()
|
||||
if svc == nil {
|
||||
return nil, classes.ErrNotBooted
|
||||
}
|
||||
page, ok := in.Record.(*models.Page)
|
||||
if !ok || page == nil {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
db := in.DB.WithContext(ctx)
|
||||
def := svc.DefaultLocale(ctx, db)
|
||||
urls := map[string]string{}
|
||||
titles := map[string]string{}
|
||||
for _, locale := range svc.Locales(ctx, db) {
|
||||
urls[locale], titles[locale] = "", ""
|
||||
}
|
||||
for locale, path := range page.LocaleURLs(def) {
|
||||
urls[locale] = path
|
||||
}
|
||||
titles[def] = page.Title
|
||||
for locale, title := range page.Titles.Get() {
|
||||
if locale != def {
|
||||
titles[locale] = title
|
||||
}
|
||||
}
|
||||
return map[string]any{"url": urls, "title": titles}, nil
|
||||
},
|
||||
Save: func(ctx context.Context, in pact.AdminFormSave) (pact.AdminFormSaveResult, error) {
|
||||
svc := p.Service()
|
||||
if svc == nil {
|
||||
return pact.AdminFormSaveResult{}, classes.ErrNotBooted
|
||||
}
|
||||
page, ok := in.Record.(*models.Page)
|
||||
if !ok || page == nil {
|
||||
return pact.AdminFormSaveResult{}, nil
|
||||
}
|
||||
var before map[string]any
|
||||
if !in.Created {
|
||||
before = classes.PageValues(*page)
|
||||
old, _ := classes.ChangesValues(in.Changes, false)
|
||||
for k, v := range old {
|
||||
before[k] = v
|
||||
}
|
||||
}
|
||||
urls, urlsSent := localeTexts(in.Values["url"])
|
||||
titles, titlesSent := localeTexts(in.Values["title"])
|
||||
if !urlsSent {
|
||||
urls = nil
|
||||
}
|
||||
if !titlesSent {
|
||||
titles = nil
|
||||
}
|
||||
if err := svc.ApplyPageTexts(ctx, in.DB, page, urls, titles); err != nil {
|
||||
return pact.AdminFormSaveResult{}, err
|
||||
}
|
||||
problems, err := svc.CheckPage(ctx, in.DB, *page)
|
||||
if err != nil {
|
||||
return pact.AdminFormSaveResult{}, err
|
||||
}
|
||||
if list := problems["url"]; len(list) > 0 {
|
||||
refusals := pact.AdminFormErrors{}
|
||||
for _, pr := range list {
|
||||
refusals["url"] = append(refusals["url"], svc.PhraseCtx(ctx, pr.Key, pr.Params))
|
||||
}
|
||||
return pact.AdminFormSaveResult{}, refusals
|
||||
}
|
||||
if err := in.DB.Model(&models.Page{}).Where("id = ?", page.ID).Updates(map[string]any{
|
||||
"url": page.URL, "urls": page.URLs, "title": page.Title, "titles": page.Titles,
|
||||
}).Error; err != nil {
|
||||
return pact.AdminFormSaveResult{}, err
|
||||
}
|
||||
return pact.AdminFormSaveResult{}, svc.RecordPage(ctx, in.DB, before, *page, false)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// localeTexts reads a submitted translatable value as locale -> text;
|
||||
// sent is false when the request did not send the field. A plain string
|
||||
// is the default locale's ("" key).
|
||||
func localeTexts(raw any) (map[string]string, bool) {
|
||||
switch v := raw.(type) {
|
||||
case nil:
|
||||
return nil, false
|
||||
case map[string]string:
|
||||
return v, true
|
||||
case map[string]any:
|
||||
out := make(map[string]string, len(v))
|
||||
keys := make([]string, 0, len(v))
|
||||
for k := range v {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
s, _ := v[k].(string)
|
||||
out[k] = s
|
||||
}
|
||||
return out, true
|
||||
case string:
|
||||
return map[string]string{"": v}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
Reference in New Issue
Block a user