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:
Jakub Zych
2026-11-04 13:26:31 +01:00
parent 26193ae4eb
commit 49d3879954
11 changed files with 283 additions and 12 deletions

View File

@@ -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
}