package seo import ( "context" "sort" "time" "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, "updated_at": time.Now().UTC(), }).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 }