- PUT /_seo/api/v1/seo writes to the record behind a URL in its locale, else to an exact URL rule (force_rule writes a rule); the answer says where (D-11) - management routes resolve the backend administrator from a token or the MCP dispatch and require golem15.seo.manage (D-10, D-15) - MCP tool set_url_seo; history records via mcp - classes.WithTx, FindOwner, SetURLSEO, SaveRule and DeleteRule
110 lines
3.2 KiB
Go
110 lines
3.2 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
|
|
}
|
|
|
|
type txKey struct{}
|
|
|
|
// WithTx returns ctx carrying tx, the transaction an API write or a batch
|
|
// runs in, so every read of the service (the resolver, the redirect set,
|
|
// the settings) sees the uncommitted changes of that transaction.
|
|
func WithTx(ctx context.Context, tx *gorm.DB) context.Context {
|
|
if tx == nil {
|
|
return ctx
|
|
}
|
|
return context.WithValue(ctx, txKey{}, tx)
|
|
}
|
|
|
|
// DB returns the transaction carried by ctx (WithTx, else the admin write
|
|
// transaction of cabana.TxFromContext), else the application's database
|
|
// bound to ctx.
|
|
func (s *Service) DB(ctx context.Context) (*gorm.DB, error) {
|
|
if tx, ok := ctx.Value(txKey{}).(*gorm.DB); ok && tx != nil {
|
|
return tx, nil
|
|
}
|
|
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
|
|
}
|
|
|
|
// GormDB returns the application's database without a transaction, the
|
|
// handle lagoon.Transaction opens a write on.
|
|
func (s *Service) GormDB() (*gorm.DB, error) {
|
|
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, nil
|
|
}
|
|
|
|
// 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
|
|
}
|