- POST /_seo/api/v1/batch runs changes in one transaction: dry_run answers the diff and the conflicts (loops, chains, noindex on a published record, canonicals to redirected or noindex addresses) and rolls back; a refused change refuses the batch (D-12) - undo of a change or a whole batch from History and through MCP, with the undo_stale, undo_loop and undo_gone refusals (D-13) - CSV export and import of redirects, rules and records by URL: BOM, comma or semicolon, formula cells escaped, a dry-run preview, all or nothing with via import; the screen's apply token is HMAC-SHA256 over the file and a fingerprint of the data (transfer.stale) - Import and export singleton with the seo_transfer widget action, History record actions undo and undo_batch, navigation entry - MCP tools undo_change, undo_batch, apply_batch, export_csv, import_csv - TestBatch, TestUndo, TestCSVTransfer, TestLoopRefused mcp and import
344 lines
14 KiB
Go
344 lines
14 KiB
Go
package seo
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
apicontrollers "git.golem15.com/golem15/sm-seo-plugin/controllers/api"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
var _ pact.HasMCP = (*Plugin)(nil)
|
|
|
|
// mcpGuard is the guard every SEO tool runs as: the backend account linked
|
|
// to the MCP user. The routes resolve that administrator themselves.
|
|
const mcpGuard = "backend"
|
|
|
|
// mcpPermission is the one SEO permission (D-15) every tool is listed with;
|
|
// the routes check it on every call.
|
|
const mcpPermission = apicontrollers.PermissionManage
|
|
|
|
// MCP declares the SEO tools (D-10, P-24): one per operation of the SEO
|
|
// admin (operations.go maps each admin operation to its tool), each bound
|
|
// to a /_seo/api/v1 management route, run as the backend account linked to
|
|
// the MCP user and listed only when that account has golem15.seo.manage.
|
|
// pier prefixes them with seo_. GET tools are read-only, so a read grant
|
|
// sees only those; deletes and "mark as gone" are destructive.
|
|
func (p *Plugin) MCP() pact.MCP {
|
|
destructive := true
|
|
return pact.MCP{Tools: []pact.MCPTool{
|
|
// Pages addressed by URL (D-11, D-13).
|
|
{
|
|
Name: "get_url_seo",
|
|
Title: "Get the SEO of a URL",
|
|
Description: "Shows the effective SEO of a page addressed by URL: every value with its source (URL rule, record, template, global settings, content or default), the redirect decision, the owner a change would be written to (the record or a URL rule) and the warnings of the SEO tab.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/seo",
|
|
Input: apicontrollers.GetSEOInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "set_url_seo",
|
|
Title: "Set the SEO of a URL",
|
|
Description: "Changes the SEO values of a page addressed by URL. A record behind the URL gets the change in its SEO tab in the URL's locale; otherwise, or with force_rule, an exact URL rule is written. The answer says where it was written.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPut,
|
|
Path: "/_seo/api/v1/seo",
|
|
Input: apicontrollers.SetSEOInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
|
|
// URL rules (D-02).
|
|
{
|
|
Name: "list_rules",
|
|
Title: "List URL rules",
|
|
Description: "Lists the URL rules (SEO for an address with or without a record behind it) by address, with a search text and the match kind.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/rules",
|
|
Input: apicontrollers.ListRulesInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "show_rule",
|
|
Title: "Show a URL rule",
|
|
Description: "Shows one URL rule by id.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/rules/{id}",
|
|
Input: apicontrollers.RuleIDInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "create_rule",
|
|
Title: "Create a URL rule",
|
|
Description: "Creates a URL rule: an exact address or a pattern where * matches any text, with the values it sets. A rule wins over the record's SEO tab.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPost,
|
|
Path: "/_seo/api/v1/rules",
|
|
Input: apicontrollers.CreateRuleInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "update_rule",
|
|
Title: "Update a URL rule",
|
|
Description: "Changes the fields sent of one URL rule by id.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPut,
|
|
Path: "/_seo/api/v1/rules/{id}",
|
|
Input: apicontrollers.UpdateRuleInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "delete_rule",
|
|
Title: "Delete a URL rule",
|
|
Description: "Deletes one URL rule by id; history keeps its values.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodDelete,
|
|
Path: "/_seo/api/v1/rules/{id}",
|
|
Input: apicontrollers.RuleIDInput{},
|
|
Permissions: []string{mcpPermission},
|
|
Destructive: &destructive,
|
|
},
|
|
|
|
// Redirects (D-16 to D-19).
|
|
{
|
|
Name: "list_redirects",
|
|
Title: "List redirects",
|
|
Description: "Lists redirects, newest change first, with a search text and the status (301, 302, 410) and origin filters.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/redirects",
|
|
Input: apicontrollers.ListRedirectsInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "show_redirect",
|
|
Title: "Show a redirect",
|
|
Description: "Shows one redirect by id.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/redirects/{id}",
|
|
Input: apicontrollers.RedirectIDInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "create_redirect",
|
|
Title: "Create a redirect",
|
|
Description: "Creates a 301, 302 or 410 redirect from a path, a pattern or a regular expression. A redirect that would create a loop is refused with redirect_loop and the loop's path; a chain is saved with a warning.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPost,
|
|
Path: "/_seo/api/v1/redirects",
|
|
Input: apicontrollers.CreateRedirectInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "update_redirect",
|
|
Title: "Update a redirect",
|
|
Description: "Changes the fields sent of one redirect by id; a loop is refused like a create.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPut,
|
|
Path: "/_seo/api/v1/redirects/{id}",
|
|
Input: apicontrollers.UpdateRedirectInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "delete_redirect",
|
|
Title: "Delete a redirect",
|
|
Description: "Deletes one redirect by id; history keeps its values.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodDelete,
|
|
Path: "/_seo/api/v1/redirects/{id}",
|
|
Input: apicontrollers.RedirectIDInput{},
|
|
Permissions: []string{mcpPermission},
|
|
Destructive: &destructive,
|
|
},
|
|
|
|
// 404 log (D-20).
|
|
{
|
|
Name: "list_not_found",
|
|
Title: "List the 404 log",
|
|
Description: "Lists the addresses that answered 404, most hits first, with whether a redirect answers them now (status open or resolved).",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/notfound",
|
|
Input: apicontrollers.ListNotFoundInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "redirect_not_found",
|
|
Title: "Redirect a 404 address",
|
|
Description: "Creates a 301 from a logged 404 address to a target. Refused when a rule already answers the address.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPost,
|
|
Path: "/_seo/api/v1/notfound/{id}/redirect",
|
|
Input: apicontrollers.RedirectNotFoundInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "gone_not_found",
|
|
Title: "Mark a 404 address as gone",
|
|
Description: "Creates a 410 for a logged 404 address, which tells search engines to drop it. Refused when a rule already answers the address.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPost,
|
|
Path: "/_seo/api/v1/notfound/{id}/gone",
|
|
Input: apicontrollers.NotFoundIDInput{},
|
|
Permissions: []string{mcpPermission},
|
|
Destructive: &destructive,
|
|
},
|
|
{
|
|
Name: "delete_not_found",
|
|
Title: "Remove a 404 log entry",
|
|
Description: "Removes one entry from the 404 log; it comes back on its next 404.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodDelete,
|
|
Path: "/_seo/api/v1/notfound/{id}",
|
|
Input: apicontrollers.NotFoundIDInput{},
|
|
Permissions: []string{mcpPermission},
|
|
Destructive: &destructive,
|
|
},
|
|
|
|
// robots.txt and AI bots (D-22, D-24, D-25, D-99).
|
|
{
|
|
Name: "show_robots",
|
|
Title: "Show robots.txt",
|
|
Description: "Shows the robots.txt rules, the AI bot switches, the file crawlers get in production and whether the site runs outside production (crawlers then get Disallow: /).",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/robots",
|
|
Input: struct{}{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "update_robots",
|
|
Title: "Update robots.txt",
|
|
Description: "Changes the robots.txt rules and the AI bot switches sent. The answer lists noindex addresses the new rules keep Googlebot from.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPut,
|
|
Path: "/_seo/api/v1/robots",
|
|
Input: apicontrollers.UpdateRobotsInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "test_robots",
|
|
Title: "Test an address against robots.txt",
|
|
Description: "Answers whether a crawler may fetch an address under the saved production robots.txt, with Google's matcher and the deciding line.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/robots/test",
|
|
Input: apicontrollers.TestRobotsInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
|
|
// Settings (D-03, D-05, D-07, D-20b, D-21, D-23, D-104).
|
|
{
|
|
Name: "show_settings",
|
|
Title: "Show the SEO settings",
|
|
Description: "Shows the SEO settings (templates, defaults, the query parameter list, indexing paths, the address form, verification) and the settings of every content type.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/settings",
|
|
Input: struct{}{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "update_settings",
|
|
Title: "Update the SEO settings",
|
|
Description: "Changes the SEO settings sent and, under types, the settings of content types by key, with the checks of the settings screen.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPut,
|
|
Path: "/_seo/api/v1/settings",
|
|
Input: apicontrollers.UpdateSettingsInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
|
|
// History (D-13).
|
|
{
|
|
Name: "list_history",
|
|
Title: "List SEO history",
|
|
Description: "Lists SEO changes, newest first, with author, time, via (admin, mcp, import, auto, undo), before and after values, filtered by via, kind or batch.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/history",
|
|
Input: apicontrollers.ListHistoryInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "show_history",
|
|
Title: "Show an SEO change",
|
|
Description: "Shows one SEO change by id with its before and after values.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/history/{id}",
|
|
Input: apicontrollers.HistoryIDInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
|
|
// Undo (D-13).
|
|
{
|
|
Name: "undo_change",
|
|
Title: "Undo an SEO change",
|
|
Description: "Restores the values before one SEO change from history; the undo is itself recorded. Refused with undo_stale when a later change touched the same values (naming who and when), undo_loop when the restore would create a redirect loop, undo_gone when the record is gone.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPost,
|
|
Path: "/_seo/api/v1/history/{id}/undo",
|
|
Input: apicontrollers.HistoryIDInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "undo_batch",
|
|
Title: "Undo a batch",
|
|
Description: "Undoes every change of a batch or an import (by batch id) as a whole, or none of them, with the refusals of undo_change.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPost,
|
|
Path: "/_seo/api/v1/history/batches/{batch}/undo",
|
|
Input: apicontrollers.HistoryBatchInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
|
|
// Bulk changes and CSV (D-12, D-13).
|
|
{
|
|
Name: "apply_batch",
|
|
Title: "Apply a batch of SEO changes",
|
|
Description: "Applies a list of changes (set_seo by URL, redirect and rule creates, updates and deletes) all or nothing. With dry_run it answers the diff and the conflicts (redirect loops and chains, noindex on a published page, a canonical pointing to a redirected page) and writes nothing; a loop refuses the whole batch.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPost,
|
|
Path: "/_seo/api/v1/batch",
|
|
Input: apicontrollers.ApplyBatchInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "export_csv",
|
|
Title: "Export SEO data as CSV",
|
|
Description: "Exports redirects, URL rules or the SEO of records (by URL) as CSV, UTF-8 with a BOM, 1000 rows per page; cells that start a formula are prefixed with an apostrophe.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/export",
|
|
Input: apicontrollers.ExportInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
{
|
|
Name: "import_csv",
|
|
Title: "Import SEO data from CSV",
|
|
Description: "Imports redirects, URL rules or the SEO of records (rows address pages by URL; no record means a URL rule) from CSV of at most 900 KB. With dry_run it previews every row as new, changed, unchanged or refused with notes; otherwise every row is applied as one batch, or nothing while a row is refused (a loop, an invalid value).",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodPost,
|
|
Path: "/_seo/api/v1/import",
|
|
Input: apicontrollers.ImportInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
|
|
// Audit (D-13, D-14 CMS part).
|
|
{
|
|
Name: "list_issues",
|
|
Title: "List SEO issues",
|
|
Description: "Lists the CMS-data SEO issues: missing and duplicate titles and descriptions, URL rules overriding a record, redirect chains, canonicals pointing to noindex or redirected addresses, and noindex addresses robots.txt blocks; each names the tool that fixes it.",
|
|
Guard: mcpGuard,
|
|
Method: http.MethodGet,
|
|
Path: "/_seo/api/v1/issues",
|
|
Input: apicontrollers.ListIssuesInput{},
|
|
Permissions: []string{mcpPermission},
|
|
},
|
|
}}
|
|
}
|