feat: management API and an MCP tool per SEO admin operation
- /_seo/api/v1 routes for the effective SEO of a URL with sources and owner, URL rules, redirects (loops refused with redirect_loop), the 404 log actions, robots.txt and its tester, settings and history - the CMS-data issue report: missing and duplicate titles and descriptions, rules over records, redirect chains, canonicals to noindex or redirected addresses, noindex addresses robots.txt blocks - 24 MCP tools on those routes (backend guard, golem15.seo.manage) - AdminOperations maps every admin operation to its tool; TestEveryAdminOperationHasTool and TestMCPReadToolsAreGET guard drift - robots.txt writes go through classes.SaveRobots from the admin too
This commit is contained in:
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
||||
"gorm.io/gorm"
|
||||
@@ -69,3 +70,67 @@ func SetSEO(service func() *classes.Service) http.HandlerFunc {
|
||||
return http.StatusOK, res, nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetSEOInput is the query of GET /_seo/api/v1/seo.
|
||||
type GetSEOInput struct {
|
||||
URL string `json:"url" query:"url" jsonschema:"the page: a path such as /blog/post or an absolute URL of the site"`
|
||||
}
|
||||
|
||||
// GetSEO serves GET /_seo/api/v1/seo?url= (D-13): the effective SEO of a
|
||||
// URL with the source of every value (rule, record, template, global,
|
||||
// content or default), the redirect decision (action redirect or gone with
|
||||
// the location), the owner a change would be written to, and the P1
|
||||
// warnings (a URL rule overriding the record, a canonical that redirects
|
||||
// or points to a noindex page). 422 for a URL that is not a path or
|
||||
// http(s) URL.
|
||||
func GetSEO(service func() *classes.Service) http.HandlerFunc {
|
||||
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
|
||||
target := r.URL.Query().Get("url")
|
||||
res, err := svc.Resolver().Resolve(ctx, target)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
db, err := read(ctx, svc)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
owner, _, err := svc.FindOwner(ctx, db, target, false)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
var status classes.StatusView
|
||||
switch {
|
||||
case owner.WrittenTo == classes.WrittenToRecord:
|
||||
if status, err = svc.RecordStatus(ctx, owner.Type, owner.ID); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
case owner.RuleID != 0:
|
||||
rule, err := classes.LoadRule(db, owner.RuleID)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
status = svc.RuleStatus(ctx, rule)
|
||||
}
|
||||
warnings := []string{}
|
||||
for _, c := range status.Callouts {
|
||||
text := strings.TrimSpace(c.Title + " " + c.Text)
|
||||
warnings = append(warnings, text)
|
||||
}
|
||||
sources := res.Sources
|
||||
if sources == nil {
|
||||
sources = map[string]string{}
|
||||
}
|
||||
return http.StatusOK, map[string]any{
|
||||
"url": res.URL,
|
||||
"action": res.Action,
|
||||
"status": res.Status,
|
||||
"location": res.Location,
|
||||
"locale": res.Locale,
|
||||
"staging": res.Staging,
|
||||
"seo": res.SEO,
|
||||
"sources": sources,
|
||||
"owner": owner,
|
||||
"warnings": warnings,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user