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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user