Files
sm-seo-plugin/classes/service.go
Jakub Zych 3dd936202f feat: robots.txt served file, AI bots and the non-production lock
- github.com/jimsmart/grobotstxt v1.0.3 (D-99, decision note in the
  framework repo)
- golem15_seo_robots singleton: rules and AI bot switches; eleven bots
  incl. Claude-User, all allowed by default
- GET /robots.txt: rules, a Disallow group per switched-off bot and the
  golem15.seo.robots_sitemaps lines; outside production the lock with
  X-Robots-Tag; never a 5xx (last good body, else allow-all)
- TestRobots on Google's matcher with the deciding line, and the
  noindex-blocked check (D-24)
2026-10-27 11:45:44 +01:00

81 lines
2.3 KiB
Go

// Package classes holds the SEO plugin's logic: the service every writer
// goes through, the entry store and the effective-SEO resolver.
package classes
import (
"context"
"errors"
"sync/atomic"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/signpost"
"gorm.io/gorm"
)
// ErrNotBooted is returned when the service has no booted application.
var ErrNotBooted = errors.New("golem15.seo: app is not booted")
// Service is the plugin's single entry point for reads and writes of SEO
// data. The admin extension, the admin controllers, the listeners, the job
// and the API all call it, so every write goes through the same checks and
// history.
type Service struct {
app *backpack.App
// robots is the last production robots.txt served, answered when the
// store fails (D-22: never a 5xx).
robots atomic.Pointer[string]
}
// NewService returns the service of a booted application. The database
// and the signpost registry are looked up when used, so a binary that runs
// a console command without a database still boots.
func NewService(app *backpack.App) (*Service, error) {
if app == nil {
return nil, ErrNotBooted
}
return &Service{app: app}, nil
}
// App returns the booted application.
func (s *Service) App() *backpack.App {
if s == nil {
return nil
}
return s.app
}
// DB returns the write transaction carried by ctx (cabana.TxFromContext),
// else the application's database bound to ctx.
func (s *Service) DB(ctx context.Context) (*gorm.DB, error) {
if tx, ok := cabana.TxFromContext(ctx); ok && tx != nil {
return tx, nil
}
if s == nil || s.app == nil {
return nil, ErrNotBooted
}
db, ok := s.app.Lookup[*gorm.DB]()
if !ok || db == nil {
return nil, errors.New("golem15.seo: database is not configured")
}
return db.WithContext(ctx), nil
}
// Registry returns the menu item type registry, nil when none is published.
func (s *Service) Registry() *signpost.Registry {
if s == nil || s.app == nil {
return nil
}
reg, _ := s.app.Lookup[*signpost.Registry]()
return reg
}
// Config returns the application config, nil when there is none.
func (s *Service) Config() *compass.Config {
if s == nil || s.app == nil {
return nil
}
return s.app.Config
}