- SEO › Auto-links (golem15.seo.autolinks, after Pages): list by phrase
with locale, origin and active filters, the disabled row state, the
form with its refusals, history through a field-less extension
- toolbar Add focus keyphrases (from_keyphrases) and
POST /autolinks/from-keyphrases
- GET/POST /autolinks, PUT /autolinks/{id}, DELETE /autolinks and the
tools seo_list_autolinks, seo_create_autolink, seo_update_autolink,
seo_delete_autolinks, seo_autolinks_from_keyphrases
- the settings tile description and the permission comment name
auto-links and the site audit
198 lines
7.4 KiB
Go
198 lines
7.4 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
|
"git.golem15.com/golem15/sm-seo-plugin/models"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ListAutolinksInput is the query of GET /_seo/api/v1/autolinks.
|
|
type ListAutolinksInput struct {
|
|
Page int `json:"page,omitempty" query:"page" jsonschema:"the page number, from 1"`
|
|
PerPage int `json:"per_page,omitempty" query:"per_page" jsonschema:"rows per page, at most 200 (50 by default)"`
|
|
Search string `json:"search,omitempty" query:"search" jsonschema:"text the phrase or the address contains"`
|
|
Locale string `json:"locale,omitempty" query:"locale" jsonschema:"a content locale code, or all for the phrases of every locale"`
|
|
Origin string `json:"origin,omitempty" query:"origin" jsonschema:"manual, keyphrase or mcp"`
|
|
Active string `json:"active,omitempty" query:"active" jsonschema:"true: active phrases only; false: inactive ones only"`
|
|
}
|
|
|
|
// CreateAutolinkInput is the body of POST /_seo/api/v1/autolinks.
|
|
type CreateAutolinkInput struct {
|
|
Phrase string `json:"phrase" jsonschema:"the phrase texts link at its first occurrence (whole words, any letter case), at least 3 characters"`
|
|
URL string `json:"url" jsonschema:"the address it links to: a path such as /grzyby/lwia-grzywa or a full http(s) address"`
|
|
Locale *string `json:"locale,omitempty" jsonschema:"the content locale it applies in; empty or all for every locale (the default)"`
|
|
Enabled *bool `json:"enabled,omitempty" jsonschema:"whether texts link it (default true)"`
|
|
Note *string `json:"note,omitempty" jsonschema:"an internal note"`
|
|
}
|
|
|
|
// UpdateAutolinkInput is PUT /_seo/api/v1/autolinks/{id}: only the fields
|
|
// sent change.
|
|
type UpdateAutolinkInput struct {
|
|
ID uint `json:"id" path:"id" jsonschema:"the auto-link id"`
|
|
Phrase *string `json:"phrase,omitempty" jsonschema:"the phrase, at least 3 characters"`
|
|
URL *string `json:"url,omitempty" jsonschema:"a path or a full http(s) address"`
|
|
Locale *string `json:"locale,omitempty" jsonschema:"a content locale code; empty or all for every locale"`
|
|
Enabled *bool `json:"enabled,omitempty" jsonschema:"whether texts link it"`
|
|
Note *string `json:"note,omitempty" jsonschema:"an internal note"`
|
|
}
|
|
|
|
// DeleteAutolinksInput is the query of DELETE /_seo/api/v1/autolinks.
|
|
type DeleteAutolinksInput struct {
|
|
IDs string `json:"ids" query:"ids" jsonschema:"the auto-link ids to delete, comma-separated, e.g. 3,4"`
|
|
}
|
|
|
|
// AutolinksFromKeyphrasesInput is POST /_seo/api/v1/autolinks/from-keyphrases
|
|
// (no fields).
|
|
type AutolinksFromKeyphrasesInput struct{}
|
|
|
|
// AutolinkJSON is an auto-link in an answer; locale is "" for every
|
|
// locale.
|
|
func AutolinkJSON(a models.Autolink) map[string]any {
|
|
out := classes.AutolinkValues(a)
|
|
out["id"] = a.ID
|
|
out["created_at"] = a.CreatedAt
|
|
out["updated_at"] = a.UpdatedAt
|
|
return out
|
|
}
|
|
|
|
// ListAutolinks serves GET /_seo/api/v1/autolinks: by phrase, filtered by
|
|
// search text, locale, origin and the active state.
|
|
func ListAutolinks(service func() *classes.Service) http.HandlerFunc {
|
|
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
|
|
db, err := read(ctx, svc)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
in := listParams(r)
|
|
q := db.Model(&models.Autolink{}).Order("lower(phrase) ASC, locale ASC, id ASC")
|
|
if in.Search != "" {
|
|
p := like(in.Search)
|
|
q = q.Where("lower(phrase) LIKE ? OR lower(url) LIKE ?", p, p)
|
|
}
|
|
query := r.URL.Query()
|
|
if l := strings.TrimSpace(query.Get("locale")); l != "" {
|
|
if l == models.AutolinkLocaleAll {
|
|
l = ""
|
|
}
|
|
q = q.Where("locale = ?", l)
|
|
}
|
|
if o := strings.TrimSpace(query.Get("origin")); o != "" {
|
|
q = q.Where("origin = ?", o)
|
|
}
|
|
switch strings.TrimSpace(query.Get("active")) {
|
|
case "true", "1":
|
|
q = q.Where("enabled")
|
|
case "false", "0":
|
|
q = q.Where("NOT enabled")
|
|
}
|
|
var rows []models.Autolink
|
|
total, err := paginate(q, in, &rows)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
out := make([]map[string]any, 0, len(rows))
|
|
for _, row := range rows {
|
|
out = append(out, AutolinkJSON(row))
|
|
}
|
|
return http.StatusOK, listBody(out, in, total), nil
|
|
})
|
|
}
|
|
|
|
// CreateAutolink serves POST /_seo/api/v1/autolinks: 201 with the phrase,
|
|
// 422 validation_failed for a short phrase, an invalid address or a
|
|
// phrase the locale has (UI-SPEC C22). The origin is mcp through MCP.
|
|
func CreateAutolink(service func() *classes.Service) http.HandlerFunc {
|
|
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
|
|
var body CreateAutolinkInput
|
|
if err := decode(r, maxBody, &body); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
row := models.NewAutolink()
|
|
row.Origin = classes.AutolinkOrigin(ctx)
|
|
classes.AutolinkWrite{Phrase: &body.Phrase, URL: &body.URL, Locale: body.Locale, Enabled: body.Enabled, Note: body.Note}.Apply(&row)
|
|
err := write(ctx, svc, func(ctx context.Context, tx *gorm.DB) error {
|
|
return svc.SaveAutolink(ctx, tx, &row)
|
|
})
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
return http.StatusCreated, map[string]any{"data": AutolinkJSON(row)}, nil
|
|
})
|
|
}
|
|
|
|
// UpdateAutolink serves PUT /_seo/api/v1/autolinks/{id}: the fields sent
|
|
// change, checked like a create.
|
|
func UpdateAutolink(service func() *classes.Service) http.HandlerFunc {
|
|
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
|
|
id, err := pathID(r, "id")
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
var body classes.AutolinkWrite
|
|
if err := decode(r, maxBody, &body); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
var row models.Autolink
|
|
err = write(ctx, svc, func(ctx context.Context, tx *gorm.DB) error {
|
|
var err error
|
|
if row, err = classes.LoadAutolink(tx, id); err != nil {
|
|
return err
|
|
}
|
|
body.Apply(&row)
|
|
return svc.SaveAutolink(ctx, tx, &row)
|
|
})
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
return http.StatusOK, map[string]any{"data": AutolinkJSON(row)}, nil
|
|
})
|
|
}
|
|
|
|
// DeleteAutolinks serves DELETE /_seo/api/v1/autolinks?ids=: the phrases
|
|
// named, each recorded in history; unknown ids are skipped.
|
|
func DeleteAutolinks(service func() *classes.Service) http.HandlerFunc {
|
|
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
|
|
ids, err := idList(r.URL.Query().Get("ids"))
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
var deleted []uint
|
|
err = write(ctx, svc, func(ctx context.Context, tx *gorm.DB) error {
|
|
var err error
|
|
deleted, err = svc.DeleteAutolinks(ctx, tx, ids)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
return http.StatusOK, map[string]any{"deleted": deleted}, nil
|
|
})
|
|
}
|
|
|
|
// AutolinksFromKeyphrases serves POST
|
|
// /_seo/api/v1/autolinks/from-keyphrases: one phrase per record whose
|
|
// focus keyphrase in a locale is not yet a phrase there (origin
|
|
// keyphrase), with the count added and the toast text.
|
|
func AutolinksFromKeyphrases(service func() *classes.Service) http.HandlerFunc {
|
|
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
|
|
added := 0
|
|
err := write(ctx, svc, func(ctx context.Context, tx *gorm.DB) error {
|
|
var err error
|
|
added, err = svc.FromKeyphrases(ctx, tx)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
message := svc.PhraseCtx(ctx, "golem15.seo::lang.autolinks.keyphrases_none", nil)
|
|
if added > 0 {
|
|
message = svc.ChoiceCtx(ctx, "golem15.seo::lang.autolinks.keyphrases_added", added, nil)
|
|
}
|
|
return http.StatusOK, map[string]any{"added": added, "message": message}, nil
|
|
})
|
|
}
|