feat: robots.txt editor and tester screen
- golem15.seo.robots singleton (UI-SPEC C10): staging callout and the served file (P4); the plugin's own extension adds the editor, the tester and the AI bots tab, saved to the robots row with history - field type robotstxt (E4, live lint, size meter; Decode refuses over 500 KB) and widget golem15-seo-robotstester (E5) on robots_test, which tests the unsaved text and switches through grobotstxt - saving rules that block a noindex address answers robots.blocks_noindex
This commit is contained in:
@@ -10,12 +10,14 @@ import (
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// The elements of the SEO field types (assets/src/elements).
|
||||
// The elements of the SEO field types and widgets (assets/src/elements).
|
||||
const (
|
||||
ElementText = "golem15-seo-text"
|
||||
ElementRobots = "golem15-seo-robots"
|
||||
ElementSERP = "golem15-seo-serp"
|
||||
ElementImage = "golem15-seo-image"
|
||||
ElementText = "golem15-seo-text"
|
||||
ElementRobots = "golem15-seo-robots"
|
||||
ElementSERP = "golem15-seo-serp"
|
||||
ElementImage = "golem15-seo-image"
|
||||
ElementRobotsTxt = "golem15-seo-robotstxt"
|
||||
ElementRobotsTester = "golem15-seo-robotstester"
|
||||
)
|
||||
|
||||
// The element bundle every SEO field type loads.
|
||||
@@ -27,13 +29,18 @@ var (
|
||||
// errRobotsInvalid is the refusal of a malformed robots value.
|
||||
const errRobotsInvalid = "golem15.seo::lang.robots.invalid"
|
||||
|
||||
// errRobotsTooLarge refuses a robots.txt over MaxRobotsBytes.
|
||||
const errRobotsTooLarge = "golem15.seo::lang.robots.too_large"
|
||||
|
||||
// FieldTypes returns the SEO admin field types:
|
||||
// - seotext and mlseotext (E1): text with a live length counter; option
|
||||
// measure: title (input) or description (textarea);
|
||||
// - seorobots and mlseorobots (E2): the robots directives editor; option
|
||||
// scope: record, rule or type; Decode refuses a malformed value;
|
||||
// - seoimage (E7): an uploaded social image stored in the media bucket,
|
||||
// its URL as the value (D-100); uploads need permission.
|
||||
// its URL as the value (D-100); uploads need permission;
|
||||
// - robotstxt (E4): the robots.txt editor with live lint; Decode refuses
|
||||
// a file over 500 KB (D-22).
|
||||
//
|
||||
// app is asked for the booted application at request time.
|
||||
func FieldTypes(app func() *backpack.App, permission string) []pact.FormFieldType {
|
||||
@@ -61,9 +68,29 @@ func FieldTypes(app func() *backpack.App, permission string) []pact.FormFieldTyp
|
||||
Scripts: elementScripts, Styles: elementStyles,
|
||||
Decode: decodeImage, Upload: UploadImage(app, permission),
|
||||
},
|
||||
{
|
||||
Name: "robotstxt", Element: ElementRobotsTxt,
|
||||
Scripts: elementScripts, Styles: elementStyles,
|
||||
Decode: decodeRobotsTxt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// decodeRobotsTxt stores a robots.txt with Unix line ends and refuses one
|
||||
// over MaxRobotsBytes (robots.too_large); lint findings are warnings in
|
||||
// the element and never block a save.
|
||||
func decodeRobotsTxt(_ context.Context, in pact.FormFieldValue) (pact.FormFieldDecoded, error) {
|
||||
s, ok := stringValue(in.Value)
|
||||
if !ok {
|
||||
return pact.FormFieldDecoded{}, &pact.FormFieldError{Messages: []string{"The " + in.Field + " field must be text."}}
|
||||
}
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
if len(s) > MaxRobotsBytes {
|
||||
return pact.FormFieldDecoded{}, &pact.FormFieldError{Messages: []string{errRobotsTooLarge}}
|
||||
}
|
||||
return pact.FormFieldDecoded{Value: s}, nil
|
||||
}
|
||||
|
||||
func checkMeasure(options map[string]any) error {
|
||||
if raw, ok := options["measure"]; ok {
|
||||
if s, _ := raw.(string); s != "title" && s != "description" {
|
||||
|
||||
Reference in New Issue
Block a user