feat: links settings and outbound rules in resolve
- migration 202610280003: settings links_new_tab (default on),
nofollow_domains and sponsored_domains; robots llms_enabled and
llms_intro (used by the llms.txt commit)
- SEO settings Links tab: the new-tab switch and the two domain lists,
one domain per line, lowercased, refused with settings.domain_invalid
naming the line; writable through seo_update_settings
- the resolve answer of an ok page carries outbound {new_tab,
nofollow_domains, sponsored_domains}; no existing key changes
This commit is contained in:
@@ -57,11 +57,14 @@ type Result struct {
|
||||
Status int `json:"status"`
|
||||
// Location is the absolute address a redirect answer sends the visitor
|
||||
// to; absent on every other action.
|
||||
Location string `json:"location,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Locale string `json:"locale"`
|
||||
Staging bool `json:"staging"`
|
||||
SEO *SEO `json:"seo"`
|
||||
Location string `json:"location,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Locale string `json:"locale"`
|
||||
Staging bool `json:"staging"`
|
||||
SEO *SEO `json:"seo"`
|
||||
// Outbound is how the layer renders links to other sites (P33-16),
|
||||
// present on ok answers.
|
||||
Outbound *Outbound `json:"outbound,omitempty"`
|
||||
Sources map[string]string `json:"-"`
|
||||
// TypeKey and RecordID name the record behind the URL, "" when none.
|
||||
TypeKey string `json:"-"`
|
||||
@@ -80,6 +83,35 @@ type SEO struct {
|
||||
Verification []MetaTag `json:"verification"`
|
||||
}
|
||||
|
||||
// Outbound holds the settings' Links rules for links to other sites
|
||||
// (D-83): open them in a new tab, and the domains (subdomains included)
|
||||
// whose links always get nofollow or sponsored.
|
||||
type Outbound struct {
|
||||
NewTab bool `json:"new_tab"`
|
||||
NofollowDomains []string `json:"nofollow_domains"`
|
||||
SponsoredDomains []string `json:"sponsored_domains"`
|
||||
}
|
||||
|
||||
// OutboundOf is the Links rules of the settings.
|
||||
func OutboundOf(s models.Settings) *Outbound {
|
||||
return &Outbound{
|
||||
NewTab: s.LinksNewTab,
|
||||
NofollowDomains: validDomains(s.NofollowDomains),
|
||||
SponsoredDomains: validDomains(s.SponsoredDomains),
|
||||
}
|
||||
}
|
||||
|
||||
// validDomains lists the valid domains of a stored list.
|
||||
func validDomains(list string) []string {
|
||||
out := []string{}
|
||||
for _, d := range models.DomainLines(list) {
|
||||
if models.ValidDomain(d) {
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// OpenGraph holds the og: values.
|
||||
type OpenGraph struct {
|
||||
Title string `json:"title"`
|
||||
@@ -646,6 +678,7 @@ func (r *Resolver) resolveAt(ctx context.Context, db *gorm.DB, settings models.S
|
||||
}
|
||||
|
||||
res.Action, res.Status, res.Locale, res.SEO = ActionOK, 200, locale, seo
|
||||
res.Outbound = OutboundOf(settings)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ var SettingsColumns = []string{
|
||||
"site_name", "twitter_site", "title_template", "home_template",
|
||||
"default_description", "default_og_image", "query_params",
|
||||
"noindex_paths", "search_paths", "url_host", "url_slash",
|
||||
"url_lowercase", "verify_google", "verify_bing", "verify_yandex",
|
||||
"url_lowercase", "links_new_tab", "nofollow_domains",
|
||||
"sponsored_domains", "verify_google", "verify_bing", "verify_yandex",
|
||||
"verify_other", "verify_files", "csv_delimiter",
|
||||
}
|
||||
|
||||
@@ -57,6 +58,9 @@ func SettingsValues(st models.Settings) map[string]any {
|
||||
"url_host": st.URLHost,
|
||||
"url_slash": st.URLSlash,
|
||||
"url_lowercase": st.URLLowercase,
|
||||
"links_new_tab": st.LinksNewTab,
|
||||
"nofollow_domains": st.NofollowDomains,
|
||||
"sponsored_domains": st.SponsoredDomains,
|
||||
"verify_google": st.VerifyGoogle,
|
||||
"verify_bing": st.VerifyBing,
|
||||
"verify_yandex": st.VerifyYandex,
|
||||
@@ -119,12 +123,22 @@ func SetSettingsValue(st *models.Settings, column string, v any) error {
|
||||
st.VerifyYandex, err = text()
|
||||
case "csv_delimiter":
|
||||
st.CSVDelimiter, err = text()
|
||||
case "nofollow_domains":
|
||||
st.NofollowDomains, err = text()
|
||||
case "sponsored_domains":
|
||||
st.SponsoredDomains, err = text()
|
||||
case "url_lowercase":
|
||||
b, ok := v.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("golem15.seo: settings %s is not a bool", column)
|
||||
}
|
||||
st.URLLowercase = b
|
||||
case "links_new_tab":
|
||||
b, ok := v.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("golem15.seo: settings %s is not a bool", column)
|
||||
}
|
||||
st.LinksNewTab = b
|
||||
case "query_params":
|
||||
var out []models.QueryParam
|
||||
if err = list(&out); err == nil {
|
||||
@@ -256,6 +270,8 @@ func (s *Service) SaveSettings(ctx context.Context, tx *gorm.DB, in SettingsWrit
|
||||
if len(problems) > 0 {
|
||||
return models.Settings{}, &ProblemsError{Problems: problems}
|
||||
}
|
||||
row.NofollowDomains = models.NormalizeDomains(row.NofollowDomains)
|
||||
row.SponsoredDomains = models.NormalizeDomains(row.SponsoredDomains)
|
||||
now := time.Now().UTC()
|
||||
row.UpdatedAt = &now
|
||||
if err := tx.Save(&row).Error; err != nil {
|
||||
|
||||
@@ -25,6 +25,9 @@ type UpdateSettingsInput struct {
|
||||
URLHost *string `json:"url_host,omitempty" jsonschema:"keep, www or bare"`
|
||||
URLSlash *string `json:"url_slash,omitempty" jsonschema:"keep, add or remove a trailing slash"`
|
||||
URLLowercase *bool `json:"url_lowercase,omitempty" jsonschema:"lowercase the ASCII letters of addresses"`
|
||||
LinksNewTab *bool `json:"links_new_tab,omitempty" jsonschema:"open links to other sites in a new tab (rel=noopener, announced to screen readers); default true"`
|
||||
NofollowDomains *string `json:"nofollow_domains,omitempty" jsonschema:"domains whose links always get nofollow, one per line, e.g. example.com (subdomains included; no https:// and no path)"`
|
||||
SponsoredDomains *string `json:"sponsored_domains,omitempty" jsonschema:"domains whose links always get sponsored, one per line (subdomains included)"`
|
||||
VerifyGoogle *string `json:"verify_google,omitempty" jsonschema:"the Google Search Console verification code"`
|
||||
VerifyBing *string `json:"verify_bing,omitempty" jsonschema:"the Bing verification code"`
|
||||
VerifyYandex *string `json:"verify_yandex,omitempty" jsonschema:"the Yandex verification code"`
|
||||
|
||||
@@ -63,7 +63,12 @@ func (c settingsAdminController) FormBeforeUpdate(ctx context.Context, model any
|
||||
if !ok || s == nil {
|
||||
return nil
|
||||
}
|
||||
return problemsError(ctx, translator(c.app), s.Problems())
|
||||
if err := problemsError(ctx, translator(c.app), s.Problems()); err != nil {
|
||||
return err
|
||||
}
|
||||
s.NofollowDomains = models.NormalizeDomains(s.NofollowDomains)
|
||||
s.SponsoredDomains = models.NormalizeDomains(s.SponsoredDomains)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormBeforeCreate checks a created row like an update.
|
||||
|
||||
@@ -75,6 +75,13 @@ settings:
|
||||
tab_indexing: Indexing
|
||||
tab_urls: Addresses
|
||||
tab_verification: Verification
|
||||
tab_links: Links
|
||||
links_new_tab: Open links to other sites in a new tab
|
||||
links_new_tab_comment: "On: links to other sites open in a new tab with rel=noopener, and screen readers hear that they do. Off: they open in the same tab. Their nofollow, sponsored and ugc attributes apply either way."
|
||||
nofollow_domains: Domains that always get nofollow
|
||||
sponsored_domains: Domains that always get sponsored
|
||||
domains_comment: One domain per line, e.g. example.com (subdomains included).
|
||||
domain_invalid: "Line :line: “:value” is not a domain. Write it without https:// and without a path."
|
||||
site_name: Site name
|
||||
site_name_comment: Replaces %site% in title templates.
|
||||
title_template: Title template
|
||||
|
||||
@@ -77,6 +77,13 @@ settings:
|
||||
tab_indexing: Indeksowanie
|
||||
tab_urls: Adresy
|
||||
tab_verification: Weryfikacja
|
||||
tab_links: Linki
|
||||
links_new_tab: Otwieraj linki do innych witryn w nowej karcie
|
||||
links_new_tab_comment: "Włączone: linki do innych witryn otwierają się w nowej karcie z rel=noopener, a czytniki ekranu o tym informują. Wyłączone: otwierają się w tej samej karcie. Atrybuty nofollow, sponsored i ugc działają w obu przypadkach."
|
||||
nofollow_domains: Domeny zawsze z nofollow
|
||||
sponsored_domains: Domeny zawsze ze sponsored
|
||||
domains_comment: Jedna domena w wierszu, np. example.com (razem z subdomenami).
|
||||
domain_invalid: "Wiersz :line: „:value” nie jest domeną. Wpisz ją bez https:// i bez ścieżki."
|
||||
site_name: Nazwa witryny
|
||||
site_name_comment: Zastępuje %site% w szablonach tytułów.
|
||||
title_template: Szablon tytułu
|
||||
|
||||
@@ -3,8 +3,10 @@ package models
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
)
|
||||
@@ -37,6 +39,7 @@ const (
|
||||
errVerifyFilenameInvalid = "golem15.seo::lang.settings.verify_filename_invalid"
|
||||
errVerifyFilenameTaken = "golem15.seo::lang.settings.verify_filename_taken"
|
||||
errTemplateUnknown = "golem15.seo::lang.settings.template_unknown"
|
||||
errDomainInvalid = "golem15.seo::lang.settings.domain_invalid"
|
||||
)
|
||||
|
||||
// QueryParam is one row of the D-07 query-parameter list: a name (a
|
||||
@@ -95,6 +98,13 @@ type Settings struct {
|
||||
VerifyOther lagoon.Jsonable[[]VerifyMeta] `gorm:"column:verify_other"`
|
||||
VerifyFiles lagoon.Jsonable[[]VerifyFile] `gorm:"column:verify_files"`
|
||||
CSVDelimiter string `gorm:"column:csv_delimiter"`
|
||||
// LinksNewTab opens links to other sites in a new tab (Links tab, on
|
||||
// by default); NofollowDomains and SponsoredDomains list the domains
|
||||
// whose links always get nofollow or sponsored, one per line,
|
||||
// subdomains included (D-83).
|
||||
LinksNewTab bool `gorm:"column:links_new_tab"`
|
||||
NofollowDomains string `gorm:"column:nofollow_domains"`
|
||||
SponsoredDomains string `gorm:"column:sponsored_domains"`
|
||||
// RoutesReportedAt is the time of the newest route report (D-33),
|
||||
// written by the report only.
|
||||
RoutesReportedAt *time.Time `gorm:"column:routes_reported_at"`
|
||||
@@ -114,7 +124,8 @@ func (Settings) Fillable() []string {
|
||||
"default_description", "default_og_image", "query_params",
|
||||
"noindex_paths", "search_paths", "url_host", "url_slash",
|
||||
"url_lowercase", "verify_google", "verify_bing", "verify_yandex",
|
||||
"verify_other", "verify_files",
|
||||
"verify_other", "verify_files", "links_new_tab", "nofollow_domains",
|
||||
"sponsored_domains",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +141,7 @@ func DefaultSettings() Settings {
|
||||
QueryParams: lagoon.Jsonable[[]QueryParam]{Data: DefaultQueryParams(), Valid: true},
|
||||
URLHost: HostKeep,
|
||||
URLSlash: SlashKeep,
|
||||
LinksNewTab: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,9 +218,82 @@ func (s Settings) Problems() map[string][]Problem {
|
||||
}
|
||||
seen[strings.ToLower(name)] = true
|
||||
}
|
||||
for field, list := range map[string]string{"nofollow_domains": s.NofollowDomains, "sponsored_domains": s.SponsoredDomains} {
|
||||
if p := DomainProblems(list); p != nil {
|
||||
out[field] = p
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// domainLabel is one label of a host name: letters (any script, written
|
||||
// in lowercase), digits and inner hyphens, at most 63 bytes.
|
||||
func domainLabel(label string) bool {
|
||||
if label == "" || len(label) > 63 || strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
|
||||
return false
|
||||
}
|
||||
for _, r := range label {
|
||||
if r != '-' && !unicode.IsDigit(r) && !(unicode.IsLetter(r) && !unicode.IsUpper(r)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidDomain reports whether d is a host name with at least two labels,
|
||||
// without a scheme, port, path or trailing dot.
|
||||
func ValidDomain(d string) bool {
|
||||
if d == "" || len(d) > 253 {
|
||||
return false
|
||||
}
|
||||
labels := strings.Split(d, ".")
|
||||
if len(labels) < 2 {
|
||||
return false
|
||||
}
|
||||
for _, l := range labels {
|
||||
if !domainLabel(l) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// DomainLines splits a domain list into its trimmed, lowercased,
|
||||
// non-empty lines, in order and each once.
|
||||
func DomainLines(list string) []string {
|
||||
out := []string{}
|
||||
seen := map[string]bool{}
|
||||
for _, line := range strings.Split(strings.ReplaceAll(list, "\r\n", "\n"), "\n") {
|
||||
d := strings.ToLower(strings.TrimSpace(line))
|
||||
if d == "" || seen[d] {
|
||||
continue
|
||||
}
|
||||
seen[d] = true
|
||||
out = append(out, d)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// NormalizeDomains rewrites a domain list as DomainLines, one per line.
|
||||
func NormalizeDomains(list string) string {
|
||||
return strings.Join(DomainLines(list), "\n")
|
||||
}
|
||||
|
||||
// DomainProblems refuses the first line of list that is not a domain
|
||||
// (settings.domain_invalid with its line number and text).
|
||||
func DomainProblems(list string) []Problem {
|
||||
n := 0
|
||||
for _, line := range strings.Split(strings.ReplaceAll(list, "\r\n", "\n"), "\n") {
|
||||
n++
|
||||
d := strings.ToLower(strings.TrimSpace(line))
|
||||
if d == "" || ValidDomain(d) {
|
||||
continue
|
||||
}
|
||||
return []Problem{{Key: errDomainInvalid, Params: map[string]string{"line": strconv.Itoa(n), "value": strings.TrimSpace(line)}}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(Settings{})
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# ===================================
|
||||
#
|
||||
# SEO settings (UI-SPEC C11): General, one tab per content type (added by
|
||||
# the plugin's form extension after General), Indexing, Addresses and
|
||||
# Verification.
|
||||
# the plugin's form extension after General), Indexing, Addresses, Links
|
||||
# (UI-SPEC C20) and Verification.
|
||||
|
||||
fields:
|
||||
_staging:
|
||||
@@ -139,6 +139,30 @@ fields:
|
||||
default: false
|
||||
tab: golem15.seo::lang.settings.tab_urls
|
||||
|
||||
links_new_tab:
|
||||
label: golem15.seo::lang.settings.links_new_tab
|
||||
comment: golem15.seo::lang.settings.links_new_tab_comment
|
||||
type: switch
|
||||
span: right
|
||||
default: true
|
||||
tab: golem15.seo::lang.settings.tab_links
|
||||
|
||||
nofollow_domains:
|
||||
label: golem15.seo::lang.settings.nofollow_domains
|
||||
comment: golem15.seo::lang.settings.domains_comment
|
||||
type: textarea
|
||||
size: small
|
||||
span: left
|
||||
tab: golem15.seo::lang.settings.tab_links
|
||||
|
||||
sponsored_domains:
|
||||
label: golem15.seo::lang.settings.sponsored_domains
|
||||
comment: golem15.seo::lang.settings.domains_comment
|
||||
type: textarea
|
||||
size: small
|
||||
span: right
|
||||
tab: golem15.seo::lang.settings.tab_links
|
||||
|
||||
verify_google:
|
||||
label: golem15.seo::lang.settings.verify_google
|
||||
comment: golem15.seo::lang.settings.verify_code_comment
|
||||
|
||||
@@ -330,3 +330,57 @@ func TestRouteReport(t *testing.T) {
|
||||
t.Fatalf("missing=only after the page is reported again = %s", again)
|
||||
}
|
||||
}
|
||||
|
||||
// outboundOf reads the resolve answer's outbound rules.
|
||||
func outboundOf(t *testing.T, got map[string]any) map[string]any {
|
||||
t.Helper()
|
||||
out, ok := got["outbound"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("resolve answer without outbound: %v", got)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestSettingsLinks covers the Links tab (UI-SPEC C20, D-83, P33-16): the
|
||||
// new-tab switch on by default, the domain lists with their line-numbered
|
||||
// refusal, the MCP-writable keys and outbound in the resolve answer.
|
||||
func TestSettingsLinks(t *testing.T) {
|
||||
env := bootEnv(t)
|
||||
token := env.admin(t, true)
|
||||
findField(t, env.formSchema(t, token, "golem15/seo/settings"), "nofollow_domains")
|
||||
env.createPage(t, token, map[string]any{"title": "About", "slug": "about", "published": true})
|
||||
|
||||
out := outboundOf(t, env.resolve(t, "/pages/about"))
|
||||
if out["new_tab"] != true || len(out["nofollow_domains"].([]any)) != 0 || len(out["sponsored_domains"].([]any)) != 0 {
|
||||
t.Fatalf("default outbound = %v, want new_tab true and no domains", out)
|
||||
}
|
||||
if got := env.resolve(t, "/pages/missing"); got["outbound"] != nil {
|
||||
t.Fatalf("a not_found answer carries outbound: %v", got)
|
||||
}
|
||||
|
||||
rec := env.call(t, http.MethodPut, adminAPI("/golem15/seo/settings/1"), token, map[string]any{"nofollow_domains": "example.com\nhttps://x.pl/a"})
|
||||
if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), `Line 2: “https://x.pl/a” is not a domain.`) {
|
||||
t.Fatalf("admin save of a bad domain = %d %s, want 422 naming line 2", rec.Code, rec.Body.String())
|
||||
}
|
||||
env.expect(t, http.StatusOK, http.MethodPut, adminAPI("/golem15/seo/settings/1"), token, map[string]any{"nofollow_domains": "Example.com\n\n shop.example.org "})
|
||||
var stored string
|
||||
if err := env.db.Raw(`SELECT nofollow_domains FROM golem15_seo_settings WHERE id = 1`).Scan(&stored).Error; err != nil || stored != "example.com\nshop.example.org" {
|
||||
t.Fatalf("stored nofollow_domains = %q err=%v", stored, err)
|
||||
}
|
||||
|
||||
api := "/_seo/api/v1/settings"
|
||||
rec = env.call(t, http.MethodPut, api, token, map[string]any{"sponsored_domains": "partner.pl\nnot a domain"})
|
||||
if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), "sponsored_domains") || !strings.Contains(rec.Body.String(), "Line 2") {
|
||||
t.Fatalf("API save of a bad domain = %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
env.expect(t, http.StatusOK, http.MethodPut, api, token, map[string]any{"sponsored_domains": "partner.pl", "links_new_tab": false})
|
||||
show := env.expect(t, http.StatusOK, http.MethodGet, api, token, nil).Body.String()
|
||||
if !jsonContains(show, `"links_new_tab":false`) || !jsonContains(show, `"sponsored_domains":"partner.pl"`) {
|
||||
t.Fatalf("settings = %s", show)
|
||||
}
|
||||
out = outboundOf(t, env.resolve(t, "/pages/about"))
|
||||
raw, _ := json.Marshal(out)
|
||||
if string(raw) != `{"new_tab":false,"nofollow_domains":["example.com","shop.example.org"],"sponsored_domains":["partner.pl"]}` {
|
||||
t.Fatalf("outbound = %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestSettingsTypeTabs(t *testing.T) {
|
||||
env := bootEnv(t)
|
||||
token := env.admin(t, true)
|
||||
fields := env.formSchema(t, token, "golem15/seo/settings")
|
||||
if got, want := strings.Join(tabsOf(fields), " | "), "General | Page | Tags | Notes | Site page | Indexing | Addresses | Verification"; got != want {
|
||||
if got, want := strings.Join(tabsOf(fields), " | "), "General | Page | Tags | Notes | Site page | Indexing | Addresses | Links | Verification"; got != want {
|
||||
t.Fatalf("settings tabs = %q, want %q", got, want)
|
||||
}
|
||||
findField(t, fields, "type_acme_page_inject")
|
||||
@@ -81,7 +81,7 @@ func TestSettingsTypeTabs(t *testing.T) {
|
||||
t.Run("no other content types", func(t *testing.T) {
|
||||
env := bootPlugins(t, []string{"golem15.seo"})
|
||||
token := env.admin(t, true)
|
||||
if got, want := strings.Join(tabsOf(env.formSchema(t, token, "golem15/seo/settings")), " | "), "General | Site page | Indexing | Addresses | Verification"; got != want {
|
||||
if got, want := strings.Join(tabsOf(env.formSchema(t, token, "golem15/seo/settings")), " | "), "General | Site page | Indexing | Addresses | Links | Verification"; got != want {
|
||||
t.Fatalf("settings tabs = %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
37
updates/202610280003_add_golem15_seo_links_and_llms.go
Normal file
37
updates/202610280003_add_golem15_seo_links_and_llms.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// addLinksAndLLMS adds the SEO settings' Links tab (links_new_tab, on by
|
||||
// default; nofollow_domains and sponsored_domains, one domain per line)
|
||||
// and the robots.txt screen's llms.txt switch (llms_enabled, off by
|
||||
// default) and introduction (llms_intro).
|
||||
var addLinksAndLLMS = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610280003_add_golem15_seo_links_and_llms",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.Exec(`ALTER TABLE golem15_seo_settings
|
||||
ADD COLUMN links_new_tab BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
ADD COLUMN nofollow_domains TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN sponsored_domains TEXT NOT NULL DEFAULT ''`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Exec(`ALTER TABLE golem15_seo_robots
|
||||
ADD COLUMN llms_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN llms_intro TEXT NOT NULL DEFAULT ''`).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
if err := tx.Exec(`ALTER TABLE golem15_seo_robots DROP COLUMN IF EXISTS llms_enabled, DROP COLUMN IF EXISTS llms_intro`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Exec(`ALTER TABLE golem15_seo_settings DROP COLUMN IF EXISTS links_new_tab, DROP COLUMN IF EXISTS nofollow_domains, DROP COLUMN IF EXISTS sponsored_domains`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(addLinksAndLLMS...)
|
||||
}
|
||||
Reference in New Issue
Block a user