feat: URL normalisation, strict URLs and the one-hop redirect decision

- Normalize: https, www or bare host, double slashes and index files,
  trailing slash add/remove, optional ASCII lowercasing (defaults change
  nothing but scheme, slashes and index files)
- strict URLs through signpost MatchPath: a path that names a record
  without being its address 301s to the record's address
- resolve runs normalisation, redirects, strict URLs, then SEO; the
  location is the final address, so a visitor makes one hop
- P1 warns when a record's canonical URL redirects
This commit is contained in:
2026-10-27 11:41:45 +01:00
parent 327925c43e
commit 4cb0fbfebb
6 changed files with 539 additions and 55 deletions

View File

@@ -220,24 +220,31 @@ func (s *Service) ruleCallout(ctx context.Context, rules []models.Rule) (Callout
return Callout{}, false
}
// canonicalCallout warns when a canonical on this site resolves to a page
// served noindex. Outside production every page is noindex, so it says
// nothing there.
// canonicalCallout warns about a canonical on this site (D-06): one that
// redirects (status.canonical_redirected, naming the final address), or
// one that resolves to a page served noindex (status.canonical_noindex).
// Outside production every page is noindex, so only the redirect warning
// applies there.
func (s *Service) canonicalCallout(ctx context.Context, canonical string) (Callout, bool) {
canonical = strings.TrimSpace(canonical)
if canonical == "" || s.Staging() {
if canonical == "" {
return Callout{}, false
}
target := canonical
if u, err := url.Parse(canonical); err == nil && u.IsAbs() {
site, err := url.Parse(s.SiteURL())
if err != nil || site.Host == "" || !strings.EqualFold(site.Host, u.Host) {
if !sameSite(s.SiteURL(), u.Host) {
return Callout{}, false
}
target = u.RequestURI()
target = u.String()
}
res, err := s.Resolver().Resolve(ctx, target)
if err != nil || res.SEO == nil || !strings.Contains(res.SEO.Robots, "noindex") {
if err != nil {
return Callout{}, false
}
if res.Action == ActionRedirect {
return Callout{Title: s.PhraseCtx(ctx, "golem15.seo::lang.status.canonical_redirected", map[string]string{"url": canonical, "target": res.Location})}, true
}
if s.Staging() || res.SEO == nil || !strings.Contains(res.SEO.Robots, "noindex") {
return Callout{}, false
}
return Callout{Title: s.PhraseCtx(ctx, "golem15.seo::lang.status.canonical_noindex", map[string]string{"url": canonical})}, true