feat: orphan pages and merged CMS issues with fix routes
- orphan_page for listed indexable pages no crawled page links to, with pages mentioning the title or focus keyphrase as suggestions (D-82) - CMS-data image_alt and h1_in_content from the content descriptors (TopHeading 2 only), stored with the crawl as source cms (D-85, D-50) - seo-page names Tools edit seo_update_page; every stored issue carries a tool or fix code with details.what (P33-38) - TestIssueMerge (ignored fingerprints survive the next crawl, fixed pages lose their issues) and TestStagingCrawl (Pitfall 12)
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/sm-seo-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -26,6 +27,13 @@ const (
|
||||
// keeps Googlebot from (D-24): an error, since the sitemap asks for a
|
||||
// page the crawler may not fetch.
|
||||
IssueSitemapBlocked = "sitemap_blocked"
|
||||
// IssueImageAlt is a record image without alternative text that is
|
||||
// not marked decorative (D-85), from the content descriptors.
|
||||
IssueImageAlt = "image_alt"
|
||||
// IssueH1InContent is an H1 in the body of a record whose page renders
|
||||
// the title as the only H1 (descriptor TopHeading 2, D-50; Journal's
|
||||
// H1 setting on).
|
||||
IssueH1InContent = "h1_in_content"
|
||||
)
|
||||
|
||||
// The severities an issue may name.
|
||||
@@ -92,7 +100,92 @@ func (s *Service) Issues(ctx context.Context) ([]Issue, error) {
|
||||
data.listed = append(data.listed, address)
|
||||
}
|
||||
sort.Strings(data.listed)
|
||||
return s.issuesOf(ctx, data, s.canonicalProblem)
|
||||
out, err := s.issuesOf(ctx, data, s.canonicalProblem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := s.ContentIssues(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(out, content...), nil
|
||||
}
|
||||
|
||||
// contentIssuesPage is the page size of the content walk.
|
||||
const contentIssuesPage = 200
|
||||
|
||||
// ContentIssues are the CMS-data checks on the content descriptors of
|
||||
// every published record (D-85, D-50): image_alt for images with an empty
|
||||
// alt that are not decorative, and h1_in_content for an H1 in the body of
|
||||
// a record whose descriptor sets TopHeading 2 (off when the type sets no
|
||||
// rule, as Journal does with its H1 setting off). Each names the record
|
||||
// type's fitting tool (ContentDescriptor.Tools: image_alt or headings,
|
||||
// else edit); "" when the type names none.
|
||||
func (s *Service) ContentIssues(ctx context.Context) ([]Issue, error) {
|
||||
reg := s.Registry()
|
||||
if reg == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var out []Issue
|
||||
for _, typ := range reg.ContentTypes() {
|
||||
if typ.Content == nil || typ.Content.Records == nil {
|
||||
continue
|
||||
}
|
||||
tool := func(ops ...string) string {
|
||||
for _, op := range ops {
|
||||
if t := strings.TrimSpace(typ.Content.Tools[op]); t != "" {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
after := ""
|
||||
for {
|
||||
page, err := reg.Records(ctx, typ.Key, after, contentIssuesPage, s.UnprefixedLocale())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, rec := range page.Records {
|
||||
if !rec.Published {
|
||||
continue
|
||||
}
|
||||
for _, l := range rec.Locales {
|
||||
if strings.TrimSpace(l.URL) == "" {
|
||||
continue
|
||||
}
|
||||
var missing []string
|
||||
for _, img := range l.Images {
|
||||
if !img.Decorative && strings.TrimSpace(img.Alt) == "" && img.URL != "" {
|
||||
missing = append(missing, img.URL)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
out = append(out, Issue{Kind: IssueImageAlt, URL: l.URL, Locale: l.Locale, Tool: tool(pact.ContentToolImageAlt, pact.ContentToolEdit), Details: map[string]any{
|
||||
"count": len(missing), "images": missing, "type": typ.Key, "id": rec.ID,
|
||||
}})
|
||||
}
|
||||
if rec.TopHeading == 2 {
|
||||
var h1 []string
|
||||
for _, h := range l.Headings {
|
||||
if h.Level == 1 {
|
||||
h1 = append(h1, h.Text)
|
||||
}
|
||||
}
|
||||
if len(h1) > 0 {
|
||||
out = append(out, Issue{Kind: IssueH1InContent, URL: l.URL, Locale: l.Locale, Tool: tool(pact.ContentToolHeadings, pact.ContentToolEdit), Details: map[string]any{
|
||||
"count": len(h1), "h1": h1, "type": typ.Key, "id": rec.ID,
|
||||
}})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if page.Next == "" || page.Next == after || len(page.Records) == 0 {
|
||||
break
|
||||
}
|
||||
after = page.Next
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// issueData is what the issue report reads from the database: the SEO
|
||||
|
||||
Reference in New Issue
Block a user