feat: robots.txt editor and tester screen
- golem15.seo.robots singleton (UI-SPEC C10): staging callout and the served file (P4); the plugin's own extension adds the editor, the tester and the AI bots tab, saved to the robots row with history - field type robotstxt (E4, live lint, size meter; Decode refuses over 500 KB) and widget golem15-seo-robotstester (E5) on robots_test, which tests the unsaved text and switches through grobotstxt - saving rules that block a noindex address answers robots.blocks_noindex
This commit is contained in:
@@ -32,6 +32,7 @@ func AdminControllers(app func() *backpack.App) []pact.AdminController {
|
||||
redirectsAdminController{app: app},
|
||||
notFoundAdminController{app: app},
|
||||
rulesAdminController{app: app},
|
||||
robotsAdminController{app: app},
|
||||
historyAdminController{app: app},
|
||||
}
|
||||
}
|
||||
|
||||
101
controllers/robots.go
Normal file
101
controllers/robots.go
Normal file
@@ -0,0 +1,101 @@
|
||||
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{}
|
||||
)
|
||||
10
controllers/robots/_robots_served.htm
Normal file
10
controllers/robots/_robots_served.htm
Normal file
@@ -0,0 +1,10 @@
|
||||
{{- with .Data -}}
|
||||
<div class="summer-markdown">
|
||||
<p>{{ trans "golem15.seo::lang.robots.served_comment" }}</p>
|
||||
<pre>{{ .Served }}</pre>
|
||||
{{- if .Staging }}
|
||||
<p><strong>{{ trans "golem15.seo::lang.robots.served_production" }}</strong></p>
|
||||
<pre>{{ .Production }}</pre>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end -}}
|
||||
6
controllers/robots/_staging.htm
Normal file
6
controllers/robots/_staging.htm
Normal file
@@ -0,0 +1,6 @@
|
||||
{{- if .Data.Staging -}}
|
||||
<div class="summer-callout summer-callout--warning" role="status">
|
||||
<p class="summer-callout__title">{{ trans "golem15.seo::lang.robots.staging_title" }}</p>
|
||||
<p class="summer-callout__text">{{ trans "golem15.seo::lang.robots.staging_text" }}</p>
|
||||
</div>
|
||||
{{- end -}}
|
||||
13
controllers/robots/config_form.yaml
Normal file
13
controllers/robots/config_form.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
# robots.txt singleton (UI-SPEC C10): the singleton footer, no delete.
|
||||
name: golem15.seo::lang.robots.title
|
||||
form: ~/plugins/golem15/seo/models/robots/fields.yaml
|
||||
modelClass: Golem15\SEO\Models\Robots
|
||||
defaultRedirect: golem15/seo/robots
|
||||
|
||||
update:
|
||||
redirect: golem15/seo/robots
|
||||
redirectClose: golem15/seo/redirects
|
||||
|
||||
messages:
|
||||
update: golem15.seo::lang.robots.title
|
||||
saved: backend::lang.messages.form.saved
|
||||
Reference in New Issue
Block a user