feat: auto-links screen, keyphrase feed and MCP tools
- 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
This commit is contained in:
@@ -14,7 +14,8 @@ import (
|
||||
)
|
||||
|
||||
// PermissionManage is the one SEO permission (D-15): the SEO tab, URL
|
||||
// rules, redirects, the 404 log, robots.txt, settings and history.
|
||||
// rules, redirects, the 404 log, pages, auto-links, the site audit,
|
||||
// robots.txt, settings and history.
|
||||
const PermissionManage = "golem15.seo.manage"
|
||||
|
||||
// The admin controller IDs.
|
||||
@@ -33,6 +34,7 @@ func AdminControllers(app func() *backpack.App) []pact.AdminController {
|
||||
notFoundAdminController{app: app},
|
||||
rulesAdminController{app: app},
|
||||
pagesAdminController{app: app},
|
||||
autolinksAdminController{app: app},
|
||||
issuesAdminController{app: app},
|
||||
crawlsAdminController{app: app},
|
||||
robotsAdminController{app: app},
|
||||
|
||||
197
controllers/api/autolinks.go
Normal file
197
controllers/api/autolinks.go
Normal file
@@ -0,0 +1,197 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
236
controllers/autolinks.go
Normal file
236
controllers/autolinks.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-seo-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AutolinksControllerID is the Auto-links screen (UI-SPEC C22).
|
||||
const AutolinksControllerID = "golem15.seo.autolinks"
|
||||
|
||||
// ActionFromKeyphrases is the Auto-links toolbar action that adds the
|
||||
// records' focus keyphrases (UI-SPEC C22).
|
||||
const ActionFromKeyphrases = "from_keyphrases"
|
||||
|
||||
// autolinksAdminController lists and edits the auto-link phrases (D-82).
|
||||
// History of a save comes from the plugin's own field-less form extension
|
||||
// on this controller, which runs inside the save transaction once the row
|
||||
// is written.
|
||||
type autolinksAdminController struct {
|
||||
app func() *backpack.App
|
||||
}
|
||||
|
||||
func (autolinksAdminController) ID() string { return AutolinksControllerID }
|
||||
func (autolinksAdminController) ModelName() string { return `Golem15\SEO\Models\Autolink` }
|
||||
func (autolinksAdminController) ConfigDir() string { return "controllers/autolinks" }
|
||||
func (autolinksAdminController) RequiredPermissions() []string {
|
||||
return []string{PermissionManage}
|
||||
}
|
||||
|
||||
// NewRecord is a phrase with the form's defaults: every locale, active,
|
||||
// entered by hand.
|
||||
func (autolinksAdminController) NewRecord() any {
|
||||
a := models.NewAutolink()
|
||||
return &a
|
||||
}
|
||||
|
||||
// FormBeforeCreate refuses a short phrase, an invalid address and a
|
||||
// phrase the locale already has (UI-SPEC C22); a create is manual.
|
||||
func (c autolinksAdminController) FormBeforeCreate(ctx context.Context, model any) error {
|
||||
if a, ok := model.(*models.Autolink); ok && a != nil {
|
||||
a.Origin = models.AutolinkOriginManual
|
||||
}
|
||||
return c.check(ctx, model)
|
||||
}
|
||||
|
||||
// FormBeforeUpdate checks an update like a create.
|
||||
func (c autolinksAdminController) FormBeforeUpdate(ctx context.Context, model any) error {
|
||||
return c.check(ctx, model)
|
||||
}
|
||||
|
||||
func (c autolinksAdminController) check(ctx context.Context, model any) error {
|
||||
a, ok := model.(*models.Autolink)
|
||||
if !ok || a == nil {
|
||||
return nil
|
||||
}
|
||||
svc, err := classes.NewService(appOf(c.app))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := database(ctx, c.app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
problems, err := svc.AutolinkProblems(ctx, db, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return problemsError(ctx, translator(c.app), problems)
|
||||
}
|
||||
|
||||
// FormAfterDelete records the deleted phrase in history inside the
|
||||
// delete's transaction.
|
||||
func (c autolinksAdminController) FormAfterDelete(ctx context.Context, model any) error {
|
||||
a, ok := model.(*models.Autolink)
|
||||
if !ok || a == nil {
|
||||
return nil
|
||||
}
|
||||
svc, err := classes.NewService(appOf(c.app))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := database(ctx, c.app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.RecordAutolink(ctx, db, classes.AutolinkValues(*a), *a, true)
|
||||
}
|
||||
|
||||
// ListRowStates marks an inactive phrase disabled (autolinks.state_off).
|
||||
func (autolinksAdminController) ListRowStates(_ context.Context, _ *gorm.DB, records []any) ([][]pact.RowState, error) {
|
||||
out := make([][]pact.RowState, len(records))
|
||||
for i, record := range records {
|
||||
if a, ok := record.(*models.Autolink); ok && a != nil && !a.Enabled {
|
||||
out[i] = []pact.RowState{pact.RowStateDisabled}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// autolinkOriginLabels are the phrase keys of the origins.
|
||||
var autolinkOriginLabels = map[string]string{
|
||||
models.AutolinkOriginManual: "golem15.seo::lang.autolinks.origin_manual",
|
||||
models.AutolinkOriginKeyphrase: "golem15.seo::lang.autolinks.origin_keyphrase",
|
||||
models.AutolinkOriginMCP: "golem15.seo::lang.autolinks.origin_mcp",
|
||||
}
|
||||
|
||||
// locales returns the site's content locales, the default first.
|
||||
func (c autolinksAdminController) locales(ctx context.Context) []string {
|
||||
svc, err := classes.NewService(appOf(c.app))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
db, err := database(ctx, c.app)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return svc.Locales(ctx, db)
|
||||
}
|
||||
|
||||
// FilterOptions serves the choices of the locale and origin filters.
|
||||
func (c autolinksAdminController) FilterOptions(scope string) []pact.Option {
|
||||
switch scope {
|
||||
case models.FilterAutolinkLocale:
|
||||
out := []pact.Option{{Value: models.AutolinkLocaleAll, Label: "golem15.seo::lang.autolinks.locale_all"}}
|
||||
for _, code := range c.locales(context.Background()) {
|
||||
out = append(out, pact.Option{Value: code, Label: strings.ToUpper(code)})
|
||||
}
|
||||
return out
|
||||
case models.FilterAutolinkOrigin:
|
||||
out := make([]pact.Option, 0, len(models.AutolinkOrigins))
|
||||
for _, o := range models.AutolinkOrigins {
|
||||
out = append(out, pact.Option{Value: o, Label: autolinkOriginLabels[o]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropdownOptions serves the locale dropdown: each content locale (the
|
||||
// empty option is every locale).
|
||||
func (c autolinksAdminController) DropdownOptions(field string) []pact.Option {
|
||||
if field != "locale" {
|
||||
return nil
|
||||
}
|
||||
codes := c.locales(context.Background())
|
||||
out := make([]pact.Option, 0, len(codes))
|
||||
for _, code := range codes {
|
||||
out = append(out, pact.Option{Value: code, Label: strings.ToUpper(code)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ListExtendRecords shows the locale (all languages when empty) and the
|
||||
// origin of the list's rows as their labels in the request locale. The
|
||||
// rows are list values only and never saved.
|
||||
func (c autolinksAdminController) ListExtendRecords(ctx context.Context, _ *gorm.DB, records []any) error {
|
||||
tr := translator(c.app)
|
||||
for _, record := range records {
|
||||
a, ok := record.(*models.Autolink)
|
||||
if !ok || a == nil {
|
||||
continue
|
||||
}
|
||||
if a.Locale == "" {
|
||||
a.Locale = Phrase(ctx, tr, "golem15.seo::lang.autolinks.locale_all", nil)
|
||||
} else {
|
||||
a.Locale = strings.ToUpper(a.Locale)
|
||||
}
|
||||
if key, ok := autolinkOriginLabels[a.Origin]; ok {
|
||||
a.Origin = Phrase(ctx, tr, key, nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminActions is the toolbar's Add focus keyphrases.
|
||||
func (c autolinksAdminController) AdminActions() []pact.AdminAction {
|
||||
return []pact.AdminAction{{
|
||||
Name: ActionFromKeyphrases,
|
||||
Label: "golem15.seo::lang.autolinks.from_keyphrases",
|
||||
Permissions: []string{PermissionManage},
|
||||
Run: func(ctx context.Context, _ pact.AdminActionInput) (pact.AdminActionResult, error) {
|
||||
svc, err := classes.NewService(appOf(c.app))
|
||||
if err != nil {
|
||||
return pact.AdminActionResult{}, err
|
||||
}
|
||||
db, err := database(ctx, c.app)
|
||||
if err != nil {
|
||||
return pact.AdminActionResult{}, err
|
||||
}
|
||||
added := 0
|
||||
err = lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
var err error
|
||||
added, err = svc.FromKeyphrases(classes.WithTx(ctx, tx), tx)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return pact.AdminActionResult{}, err
|
||||
}
|
||||
return pact.AdminActionResult{Message: KeyphrasesMessage(ctx, c.app, added)}, nil
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// KeyphrasesMessage is the Add focus keyphrases toast: the count added,
|
||||
// or that there was nothing to add.
|
||||
func KeyphrasesMessage(ctx context.Context, app func() *backpack.App, added int) string {
|
||||
tr := translator(app)
|
||||
if added == 0 {
|
||||
return Phrase(ctx, tr, "golem15.seo::lang.autolinks.keyphrases_none", nil)
|
||||
}
|
||||
if tr == nil {
|
||||
return "golem15.seo::lang.autolinks.keyphrases_added"
|
||||
}
|
||||
return tr.Choice(ctx, "golem15.seo::lang.autolinks.keyphrases_added", added, nil)
|
||||
}
|
||||
|
||||
var (
|
||||
_ pact.AdminController = autolinksAdminController{}
|
||||
_ pact.AdminPermissioned = autolinksAdminController{}
|
||||
_ pact.AdminRecordSource = autolinksAdminController{}
|
||||
_ pact.FormBeforeCreate = autolinksAdminController{}
|
||||
_ pact.FormBeforeUpdate = autolinksAdminController{}
|
||||
_ pact.FormAfterDelete = autolinksAdminController{}
|
||||
_ pact.ListRowStates = autolinksAdminController{}
|
||||
_ pact.FilterOptions = autolinksAdminController{}
|
||||
_ pact.ListExtendRecords = autolinksAdminController{}
|
||||
_ pact.DropdownOptionsProvider = autolinksAdminController{}
|
||||
_ pact.HasAdminActions = autolinksAdminController{}
|
||||
)
|
||||
16
controllers/autolinks/config_filter.yaml
Normal file
16
controllers/autolinks/config_filter.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Auto-links filters (UI-SPEC C22): locale, origin and active only.
|
||||
scopes:
|
||||
locale:
|
||||
label: golem15.seo::lang.autolinks.locale
|
||||
modelClass: Golem15\SEO\Models\Autolink
|
||||
nameFrom: locale
|
||||
scope: FilterAutolinkLocale
|
||||
origin:
|
||||
label: golem15.seo::lang.autolinks.origin
|
||||
modelClass: Golem15\SEO\Models\Autolink
|
||||
nameFrom: origin
|
||||
scope: FilterAutolinkOrigin
|
||||
active:
|
||||
label: golem15.seo::lang.autolinks.filter_active
|
||||
type: switch
|
||||
column: enabled
|
||||
18
controllers/autolinks/config_form.yaml
Normal file
18
controllers/autolinks/config_form.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
# Auto-link form (UI-SPEC C22).
|
||||
name: golem15.seo::lang.autolinks.name
|
||||
form: ~/plugins/golem15/seo/models/autolink/fields.yaml
|
||||
modelClass: Golem15\SEO\Models\Autolink
|
||||
defaultRedirect: golem15/seo/autolinks
|
||||
|
||||
create:
|
||||
redirect: golem15/seo/autolinks/update/:id
|
||||
redirectClose: golem15/seo/autolinks
|
||||
|
||||
update:
|
||||
redirect: golem15/seo/autolinks
|
||||
redirectClose: golem15/seo/autolinks
|
||||
|
||||
messages:
|
||||
create: golem15.seo::lang.autolinks.new
|
||||
update: golem15.seo::lang.autolinks.name
|
||||
deleteConfirm: golem15.seo::lang.autolinks.delete_confirm
|
||||
26
controllers/autolinks/config_list.yaml
Normal file
26
controllers/autolinks/config_list.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Auto-links list (UI-SPEC C22): the phrases by phrase; the toolbar
|
||||
# creates, deletes and adds the records' focus keyphrases.
|
||||
list: ~/plugins/golem15/seo/models/autolink/columns.yaml
|
||||
modelClass: Golem15\SEO\Models\Autolink
|
||||
title: golem15.seo::lang.autolinks.list_title
|
||||
recordUrl: golem15/seo/autolinks/update/:id
|
||||
noRecordsMessage: golem15.seo::lang.autolinks.list_empty
|
||||
recordsPerPage: 50
|
||||
showCheckboxes: true
|
||||
showSetup: true
|
||||
filter: config_filter.yaml
|
||||
|
||||
defaultSort:
|
||||
column: phrase
|
||||
direction: asc
|
||||
|
||||
toolbar:
|
||||
buttons: [create, delete, from_keyphrases]
|
||||
search:
|
||||
prompt: backend::lang.list.search_prompt
|
||||
|
||||
messages:
|
||||
create: golem15.seo::lang.autolinks.new
|
||||
empty: golem15.seo::lang.autolinks.list_empty
|
||||
deleteConfirm: golem15.seo::lang.autolinks.delete_selected_confirm
|
||||
rowStateDisabled: golem15.seo::lang.autolinks.state_off
|
||||
Reference in New Issue
Block a user