package api import ( "context" "net/http" "git.golem15.com/golem15/sm-seo-plugin/classes" "gorm.io/gorm" ) // SEOValues are the SEO values of a URL in a request: a value that is // absent is left as it is, an empty one inherits. type SEOValues struct { Title *string `json:"title,omitempty" jsonschema:"the SEO title"` Description *string `json:"description,omitempty" jsonschema:"the meta description"` Keyphrase *string `json:"keyphrase,omitempty" jsonschema:"the focus keyphrase (records only)"` Canonical *string `json:"canonical,omitempty" jsonschema:"the canonical URL: a path or an absolute http(s) URL"` Robots *string `json:"robots,omitempty" jsonschema:"robots directives, for example noindex,follow"` OGTitle *string `json:"og_title,omitempty" jsonschema:"the social title"` OGDescription *string `json:"og_description,omitempty" jsonschema:"the social description"` OGImage *string `json:"og_image,omitempty" jsonschema:"the social image: a path or an absolute URL"` } // Map returns the values that are set, keyed by column. func (v SEOValues) Map() map[string]string { out := map[string]string{} for column, p := range map[string]*string{ "title": v.Title, "description": v.Description, "keyphrase": v.Keyphrase, "canonical": v.Canonical, "robots": v.Robots, "og_title": v.OGTitle, "og_description": v.OGDescription, "og_image": v.OGImage, } { if p != nil { out[column] = *p } } return out } // SetSEOInput is the body of PUT /_seo/api/v1/seo. type SetSEOInput struct { URL string `json:"url" jsonschema:"the page: a path such as /blog/post or an absolute URL of the site"` Locale string `json:"locale,omitempty" jsonschema:"the locale of a record's SEO; the locale the URL matches when omitted"` Values SEOValues `json:"values" jsonschema:"the values to change"` ForceRule bool `json:"force_rule,omitempty" jsonschema:"write a URL rule even when a record is behind the URL"` } // SetSEO serves PUT /_seo/api/v1/seo (D-11, P-25): the values go to the // record behind the URL in its locale, else to an exact URL rule (created // when missing), or to a rule when force_rule is set. It answers 200 with // where the change was written ({"written_to":"record"|"rule", "type", // "id", "locale", "rule_id"}) and the stored values; 422 for a URL that is // not a path or http(s) URL or for invalid values. History records the // write with via mcp when it comes through MCP. func SetSEO(service func() *classes.Service) http.HandlerFunc { return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) { var in SetSEOInput if err := decode(r, maxBody, &in); err != nil { return 0, nil, err } var res classes.URLSEOResult err := write(ctx, svc, func(ctx context.Context, tx *gorm.DB) error { var err error res, err = svc.SetURLSEO(ctx, tx, classes.URLSEOWrite{URL: in.URL, Locale: in.Locale, Values: in.Values.Map(), ForceRule: in.ForceRule}) return err }) if err != nil { return 0, nil, err } return http.StatusOK, res, nil }) }