feat: sitemap-aware batch conflict and the sitemap_blocked issue

- noindex_published fires only when a sitemap file lists the address now
  (read once per batch through contract.Listing)
- CMS issue sitemap_blocked (severity error, tool seo_update_robots,
  details line and rule): a listed address the production robots.txt
  keeps Googlebot from
- the SEO tab's P1 shows status.excluded_title with the reason (noindex,
  canonical elsewhere, redirect, or the type switched off in the sitemap
  settings via ListingSettings.TypesOff) as a plain callout
- robots.txt is composed once per check of many addresses
This commit is contained in:
Jakub Zych
2026-11-04 13:39:31 +01:00
parent 742a5b930b
commit 59f0457c69
11 changed files with 370 additions and 22 deletions

View File

@@ -290,8 +290,9 @@ func (s *Service) runBatch(ctx context.Context, opts BatchOptions, build func(ct
}
res.Changes = append(res.Changes, item)
}
listed := &listedOnce{svc: s}
for i := range res.Changes {
late, err := s.batchConflicts(ctx, tx, i, changes[i], res.Changes[i])
late, err := s.batchConflicts(ctx, tx, listed, i, changes[i], res.Changes[i])
if err != nil {
return err
}
@@ -452,12 +453,34 @@ func (s *Service) applyChange(ctx context.Context, tx *gorm.DB, index int, ch Ba
return errUnknownOp
}
// listedOnce reads the addresses the sitemaps list once per batch.
type listedOnce struct {
svc *Service
read bool
ok bool
listed map[string]bool
}
// has reports whether the sitemaps list address; ok is false without the
// sitemap plugin's Listing.
func (l *listedOnce) has(ctx context.Context, address string) (listed, ok bool, err error) {
if !l.read {
l.listed, l.ok, err = l.svc.SitemapListed(context.WithoutCancel(ctx))
if err != nil {
return false, false, err
}
l.read = true
}
return l.listed[address], l.ok, nil
}
// batchConflicts are the warnings that depend on the whole batch, checked
// once every change is in place: noindex on the address of a published
// record (the records the sitemaps list; Phase 33's sitemap filter
// refines it) and a canonical pointing to a redirected or noindex
// address.
func (s *Service) batchConflicts(ctx context.Context, tx *gorm.DB, index int, ch BatchChange, item BatchItem) ([]Conflict, error) {
// record that a sitemap file lists now (read through contract.Listing, so
// an address the sitemaps already leave out, such as one of a content
// type switched off, is no conflict; D-12, D-24), and a canonical pointing
// to a redirected or noindex address.
func (s *Service) batchConflicts(ctx context.Context, tx *gorm.DB, listed *listedOnce, index int, ch BatchChange, item BatchItem) ([]Conflict, error) {
if item.Error != "" {
return nil, nil
}
@@ -490,7 +513,15 @@ func (s *Service) batchConflicts(ctx context.Context, tx *gorm.DB, index int, ch
if err != nil && !errors.Is(err, ErrBadURL) {
return nil, err
}
if err == nil && owner.WrittenTo == WrittenToRecord && owner.Published {
inSitemap := err == nil && owner.WrittenTo == WrittenToRecord && owner.Published
if inSitemap {
has, ok, lerr := listed.has(ctx, owner.URL)
if lerr != nil {
return nil, lerr
}
inSitemap = !ok || has
}
if inSitemap {
out = append(out, Conflict{Change: index, Kind: ConflictNoindexPublished, URL: owner.URL,
Message: s.PhraseCtx(ctx, "golem15.seo::lang.api.conflict_noindex_published", map[string]string{"url": owner.URL})})
}