Files
sm-seo-plugin/models/robots.go
Jakub Zych 742a5b930b feat: site endpoint, llms.txt and the sitemap index in robots.txt
- 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
2026-11-04 13:32:12 +01:00

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{})
}