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/pact" "gorm.io/gorm" "gorm.io/gorm/clause" ) // RobotsControllerID is the robots.txt screen (UI-SPEC C10). const RobotsControllerID = "golem15.seo.robots" // robotsAdminController edits the robots.txt singleton (D-22, D-24, // D-25). Its form holds the staging callout and the served file (P4); the // editor (E4), the tester (E5) and the AI bot switches come from the // plugin's own form extension on this controller, so the tester can read // the unsaved editor text and switches. type robotsAdminController struct { app func() *backpack.App } func (robotsAdminController) ID() string { return RobotsControllerID } func (robotsAdminController) ModelName() string { return `Golem15\SEO\Models\Robots` } func (robotsAdminController) ConfigDir() string { return "controllers/robots" } func (robotsAdminController) NewRecord() any { return &models.Robots{} } func (robotsAdminController) RequiredPermissions() []string { return []string{PermissionManage} } // SingletonRecord returns robots row 1, creating it when the migration's // row is gone (first or create, safe against a concurrent first request). func (robotsAdminController) SingletonRecord(_ context.Context, db *gorm.DB) (any, error) { if db == nil { return nil, errors.New("golem15.seo: database is not configured") } var rows []models.Robots 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.Robots{ID: 1, Bots: models.BotSwitches{}} 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: robots row is missing") } return &rows[0], nil } // StagingView is the staging callout partial's view model. type StagingView struct { Staging bool } // ServedView is P4: the file crawlers get and, outside production, the // production version under it. type ServedView struct { Served string Production string Staging bool } // PartialData serves the staging callout and P4. func (c robotsAdminController) PartialData(ctx context.Context, name string, _ any) (any, error) { svc, err := classes.NewService(appOf(c.app)) if err != nil { return nil, err } staging := svc.Staging() switch name { case "staging": return StagingView{Staging: staging}, nil case "robots_served": production, err := svc.ServedFile(ctx, true) if err != nil { return nil, err } view := ServedView{Served: production, Staging: staging} if staging { view.Served, view.Production = classes.NonProductionRobots, production } return view, nil } return nil, nil } var ( _ pact.AdminController = robotsAdminController{} _ pact.AdminPermissioned = robotsAdminController{} _ pact.AdminRecordSource = robotsAdminController{} _ pact.AdminSingleton = robotsAdminController{} _ pact.AdminPartialData = robotsAdminController{} )