- GET /_seo/api/v1/site (public, noindex): site_url, default_locale, locales, unprefixed_locale, links.new_tab, html_sitemap (from the sitemap plugin's Listing), routes of every content type, root_files (verification files and the IndexNow key file) and llms - GET /llms.txt: with llms_enabled (robots.txt screen, AI bots tab, off by default) and in production, the indexable pages from the sitemaps per group under the site name and llms_intro (else the default description); otherwise 404; seo_update_robots takes both fields - robots.txt names the sitemap plugin's index unless golem15.seo.robots_sitemaps is set; the config default is now a comment
87 lines
2.4 KiB
Go
87 lines
2.4 KiB
Go
package models
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// Robots is the robots.txt singleton (row id 1) on golem15_seo_robots
|
|
// (D-22, D-25): the editor's rules and the AI bot switches. The served
|
|
// file adds a Disallow group for every switched-off bot and the Sitemap
|
|
// lines; outside production it is replaced by the non-production lock.
|
|
type Robots struct {
|
|
ID uint `gorm:"column:id;primaryKey"`
|
|
Content string `gorm:"column:content"`
|
|
Bots BotSwitches `gorm:"column:bots"`
|
|
// LlmsEnabled serves /llms.txt (D-25, off by default); LlmsIntro is
|
|
// its introduction, the default meta description when empty.
|
|
LlmsEnabled bool `gorm:"column:llms_enabled"`
|
|
LlmsIntro string `gorm:"column:llms_intro"`
|
|
CreatedAt *time.Time `gorm:"column:created_at"`
|
|
UpdatedAt *time.Time `gorm:"column:updated_at"`
|
|
}
|
|
|
|
func (Robots) TableName() string { return "golem15_seo_robots" }
|
|
|
|
// MorphName names the robots row in history.
|
|
func (Robots) MorphName() string { return `Golem15\SEO\Models\Robots` }
|
|
|
|
// Fillable lists the form's columns: the rules. The bot switches are
|
|
// written by the robots form's extension.
|
|
func (Robots) Fillable() []string { return []string{"content"} }
|
|
|
|
// Rules is empty: the robotstxt field type refuses an oversized file.
|
|
func (Robots) Rules() map[string]string { return map[string]string{} }
|
|
|
|
// BotSwitches maps a bot's user-agent token to whether it is allowed. A
|
|
// bot that is not listed is allowed (D-25: every bot on by default).
|
|
type BotSwitches map[string]bool
|
|
|
|
// Allowed reports whether token is switched on.
|
|
func (b BotSwitches) Allowed(token string) bool {
|
|
on, ok := b[token]
|
|
return !ok || on
|
|
}
|
|
|
|
// Value encodes the switches as a JSON object; nil is {}.
|
|
func (b BotSwitches) Value() (driver.Value, error) {
|
|
if b == nil {
|
|
return "{}", nil
|
|
}
|
|
raw, err := json.Marshal(map[string]bool(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return string(raw), nil
|
|
}
|
|
|
|
// Scan decodes a jsonb object; NULL reads as none.
|
|
func (b *BotSwitches) Scan(src any) error {
|
|
var raw []byte
|
|
switch v := src.(type) {
|
|
case nil:
|
|
*b = BotSwitches{}
|
|
return nil
|
|
case []byte:
|
|
raw = v
|
|
case string:
|
|
raw = []byte(v)
|
|
default:
|
|
return errors.New("models: unsupported bots value")
|
|
}
|
|
out := map[string]bool{}
|
|
if len(raw) > 0 {
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
*b = out
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
Register(Robots{})
|
|
}
|