Files
sm-seo-plugin/extensions_pages.go
Jakub Zych dfc85bc805 fix: store registry page timestamps in UTC
- GORM stamped created_at and updated_at in the process's local time,
  which the timestamp columns keep as the wall clock; east of UTC a page
  without a content date got a sitemap lastmod in the future
- Page.BeforeSave stamps both in UTC for the admin's and the API's saves,
  and the admin's address change writes updated_at in UTC
2026-11-04 19:24:15 +01:00

144 lines
4.3 KiB
Go

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
}