package controllers import ( "context" "errors" "git.golem15.com/golem15/sm-seo-plugin/classes" "git.golem15.com/golem15/sm-seo-plugin/models" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/diary" "git.golem15.com/golem15/summercms/modules/lagoon" "git.golem15.com/golem15/summercms/modules/pact" "gorm.io/gorm" "gorm.io/gorm/clause" ) // log is the controllers' logger (component=seo). var log = diary.For("seo") // settingsAdminController edits the SEO settings singleton (D-104), linked // from the SEO settings tile. Its per-content-type tabs are added by the // plugin's own form extension on this controller. type settingsAdminController struct { app func() *backpack.App } func (settingsAdminController) ID() string { return SettingsControllerID } func (settingsAdminController) ModelName() string { return `Golem15\SEO\Models\Settings` } func (settingsAdminController) ConfigDir() string { return "controllers/settings" } func (settingsAdminController) NewRecord() any { return &models.Settings{} } func (settingsAdminController) RequiredPermissions() []string { return []string{PermissionManage} } // SingletonRecord returns settings row 1, creating it with the defaults // when the migration's row is gone (first or create, safe against a // concurrent first request through ON CONFLICT DO NOTHING). func (settingsAdminController) SingletonRecord(_ context.Context, db *gorm.DB) (any, error) { if db == nil { return nil, errors.New("golem15.seo: database is not configured") } var rows []models.Settings if err := db.Where("id = ?", 1).Limit(1).Find(&rows).Error; err != nil { return nil, err } if len(rows) > 0 { return &rows[0], nil } row := models.DefaultSettings() if err := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&row).Error; err != nil { return nil, err } if err := db.Where("id = ?", 1).Limit(1).Find(&rows).Error; err != nil || len(rows) == 0 { return nil, errors.New("golem15.seo: settings row is missing") } return &rows[0], nil } // FormBeforeUpdate refuses templates with unknown placeholders, malformed // or duplicate verification file names and Site audit values outside their // ranges. func (c settingsAdminController) FormBeforeUpdate(ctx context.Context, model any) error { s, ok := model.(*models.Settings) if !ok || s == nil { return nil } problems := s.Problems() for field, list := range s.AuditProblems() { problems[field] = append(problems[field], list...) } if err := problemsError(ctx, translator(c.app), problems); err != nil { return err } s.NofollowDomains = models.NormalizeDomains(s.NofollowDomains) s.SponsoredDomains = models.NormalizeDomains(s.SponsoredDomains) return nil } // FormBeforeCreate checks a created row like an update. func (c settingsAdminController) FormBeforeCreate(ctx context.Context, model any) error { return c.FormBeforeUpdate(ctx, model) } // FormAfterUpdate queues the entry generation job once the save commits, // so the effective title and description cache follows new templates. A // queue error is logged and never fails the save. func (c settingsAdminController) FormAfterUpdate(ctx context.Context, _ any) error { app := appOf(c.app) svc, err := classes.NewService(app) if err != nil { return nil } tx, err := database(ctx, c.app) if err != nil { return nil } lagoon.AfterCommit(ctx, tx, func(ctx context.Context, _ *gorm.DB) { db, ok := app.Lookup[*gorm.DB]() if !ok || db == nil { return } if _, err := svc.EnqueueGenerate(ctx, db.WithContext(ctx), ""); err != nil { log.Warn("golem15.seo: queue entry generation", "error", err) } }) return nil } // stagingView is the view model of the staging partial. type stagingView struct { Staging bool } // PartialData serves the staging callout, shown outside production where // every page is noindex whatever the settings say (D-22), the P3 // placeholder table and the P8 publisher preview. func (c settingsAdminController) PartialData(ctx context.Context, name string, _ any) (any, error) { switch name { case "staging": return stagingView{Staging: IsStaging(appOf(c.app))}, nil case "placeholders": svc, err := classes.NewService(appOf(c.app)) if err != nil { return nil, err } return svc.Placeholders(ctx), nil case "publisher_preview": svc, err := classes.NewService(appOf(c.app)) if err != nil { return nil, err } return svc.PublisherPreview(ctx) } return nil, nil } // IsStaging reports whether the application runs outside production. It // depends on the environment only (D-22): no setting can change it. func IsStaging(app *backpack.App) bool { if app == nil || app.Config == nil { return true } return app.Config.Environment() != "production" } var ( _ pact.AdminController = settingsAdminController{} _ pact.AdminPermissioned = settingsAdminController{} _ pact.AdminRecordSource = settingsAdminController{} _ pact.AdminSingleton = settingsAdminController{} _ pact.AdminPartialData = settingsAdminController{} _ pact.FormBeforeCreate = settingsAdminController{} _ pact.FormBeforeUpdate = settingsAdminController{} _ pact.FormAfterUpdate = settingsAdminController{} )