- golem15_seo_crawls, crawl pages, links, issues and ignores (P33-37) - classes/crawl: fetchguard client on the site host and its www twin, sitemap index and robots.txt seeds, two workers with a delay, limits - h1_missing and h1_multiple with the P33-38 fix route - POST /_seo/api/v1/crawls queues the golem15.seo.crawl job; GET /issues answers the stored issues with id, severity, source, fix, found_at, ignored and crawl_id
152 lines
5.6 KiB
Go
152 lines
5.6 KiB
Go
package crawl
|
|
|
|
import (
|
|
"git.golem15.com/golem15/sm-seo-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
// The crawl kinds of UI-SPEC C23.
|
|
const (
|
|
KindPageStatus = "page_status"
|
|
KindSitemapError = "sitemap_error"
|
|
KindSitemapNoindex = "sitemap_noindex"
|
|
KindSitemapBlocked = "sitemap_blocked"
|
|
KindSitemapMissing = "sitemap_missing"
|
|
KindBrokenLink = "broken_link"
|
|
KindRedirectedLink = "redirected_link"
|
|
KindCanonicalMissing = "canonical_missing"
|
|
KindCanonicalError = "canonical_error"
|
|
KindMissingTitle = "missing_title"
|
|
KindDuplicateTitle = "duplicate_title"
|
|
KindMissingDesc = "missing_description"
|
|
KindDuplicateDesc = "duplicate_description"
|
|
KindH1Missing = "h1_missing"
|
|
KindH1Multiple = "h1_multiple"
|
|
KindHeadingSkipped = "heading_skipped"
|
|
KindHeadingEmpty = "heading_empty"
|
|
KindH1TitleMismatch = "h1_title_mismatch"
|
|
KindJSONLDInvalid = "jsonld_invalid"
|
|
KindJSONLDMultiple = "jsonld_multiple"
|
|
KindJSONLDRequired = "jsonld_required"
|
|
KindJSONLDRecommended = "jsonld_recommended"
|
|
KindHreflangRecip = "hreflang_reciprocal"
|
|
KindHreflangError = "hreflang_error"
|
|
KindGenericAnchor = "generic_anchor"
|
|
KindImageAlt = "image_alt"
|
|
KindImageSize = "image_size"
|
|
KindFeaturedLazy = "featured_lazy"
|
|
KindContentJS = "content_js"
|
|
KindOrphanPage = "orphan_page"
|
|
// KindH1InContent is the CMS-data check of a record whose body holds
|
|
// an H1 although its page renders the title as the H1.
|
|
KindH1InContent = "h1_in_content"
|
|
)
|
|
|
|
// severities are the default severities of the kinds table (UI-SPEC C23),
|
|
// the Phase 32 CMS kinds included.
|
|
var severities = map[string]string{
|
|
KindPageStatus: models.SeverityError,
|
|
KindSitemapError: models.SeverityError,
|
|
KindSitemapNoindex: models.SeverityError,
|
|
KindSitemapBlocked: models.SeverityError,
|
|
KindSitemapMissing: models.SeverityNotice,
|
|
KindBrokenLink: models.SeverityError,
|
|
KindRedirectedLink: models.SeverityNotice,
|
|
KindCanonicalMissing: models.SeverityWarning,
|
|
KindCanonicalError: models.SeverityError,
|
|
"canonical_noindex": models.SeverityError,
|
|
"canonical_redirected": models.SeverityWarning,
|
|
KindMissingTitle: models.SeverityWarning,
|
|
KindDuplicateTitle: models.SeverityWarning,
|
|
KindMissingDesc: models.SeverityNotice,
|
|
KindDuplicateDesc: models.SeverityNotice,
|
|
"rule_overrides_record": models.SeverityNotice,
|
|
"redirect_chain": models.SeverityWarning,
|
|
"noindex_blocked": models.SeverityError,
|
|
KindH1Missing: models.SeverityError,
|
|
KindH1Multiple: models.SeverityError,
|
|
KindH1InContent: models.SeverityWarning,
|
|
KindHeadingSkipped: models.SeverityWarning,
|
|
KindHeadingEmpty: models.SeverityWarning,
|
|
KindH1TitleMismatch: models.SeverityNotice,
|
|
KindJSONLDInvalid: models.SeverityError,
|
|
KindJSONLDMultiple: models.SeverityWarning,
|
|
KindJSONLDRequired: models.SeverityError,
|
|
KindJSONLDRecommended: models.SeverityNotice,
|
|
KindHreflangRecip: models.SeverityWarning,
|
|
KindHreflangError: models.SeverityError,
|
|
KindGenericAnchor: models.SeverityWarning,
|
|
KindImageAlt: models.SeverityWarning,
|
|
KindImageSize: models.SeverityNotice,
|
|
KindFeaturedLazy: models.SeverityNotice,
|
|
KindContentJS: models.SeverityWarning,
|
|
KindOrphanPage: models.SeverityWarning,
|
|
}
|
|
|
|
// Severity returns a kind's severity (warning for an unknown kind).
|
|
func Severity(kind string) string {
|
|
if s, ok := severities[kind]; ok {
|
|
return s
|
|
}
|
|
return models.SeverityWarning
|
|
}
|
|
|
|
// Kinds lists every kind the audit stores, in the kinds table order.
|
|
func Kinds() []string {
|
|
return []string{
|
|
KindPageStatus, KindSitemapError, KindSitemapNoindex, KindSitemapBlocked,
|
|
KindSitemapMissing, KindBrokenLink, KindRedirectedLink, KindCanonicalMissing,
|
|
KindCanonicalError, "canonical_noindex", "canonical_redirected", KindMissingTitle,
|
|
KindDuplicateTitle, KindMissingDesc, KindDuplicateDesc, "rule_overrides_record",
|
|
"redirect_chain", "noindex_blocked", KindH1Missing, KindH1Multiple, KindH1InContent,
|
|
KindHeadingSkipped, KindHeadingEmpty, KindH1TitleMismatch, KindJSONLDInvalid,
|
|
KindJSONLDMultiple, KindJSONLDRequired, KindJSONLDRecommended, KindHreflangRecip,
|
|
KindHreflangError, KindGenericAnchor, KindImageAlt, KindImageSize, KindFeaturedLazy,
|
|
KindContentJS, KindOrphanPage,
|
|
}
|
|
}
|
|
|
|
// Env is what the checks know besides the crawl: the record behind an
|
|
// address.
|
|
type Env struct {
|
|
// Lookup returns the CMS record of an address, nil when none.
|
|
Lookup func(key string) *Record
|
|
}
|
|
|
|
func (e *Env) record(key string) *Record {
|
|
if e == nil || e.Lookup == nil {
|
|
return nil
|
|
}
|
|
return e.Lookup(key)
|
|
}
|
|
|
|
// Check runs the per-page checks over a crawl's pages.
|
|
func Check(res *Result, env *Env) []Finding {
|
|
var out []Finding
|
|
for _, key := range res.Order {
|
|
p := res.Pages[key]
|
|
if !p.OK() {
|
|
continue
|
|
}
|
|
out = append(out, checkHeadings(p, env.record(key))...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// checkHeadings reports h1_missing and h1_multiple. An H1 inside the
|
|
// record's own content is fixed through the record's headings tool.
|
|
func checkHeadings(p *Page, rec *Record) []Finding {
|
|
h1 := p.Facts.H1()
|
|
switch {
|
|
case len(h1) == 0:
|
|
return []Finding{{Kind: KindH1Missing, URL: p.Key, Record: rec}}
|
|
case len(h1) > 1:
|
|
f := Finding{Kind: KindH1Multiple, URL: p.Key, Record: rec, Details: map[string]any{"count": len(h1), "h1": h1}}
|
|
if rec.hasHeading(1, "") {
|
|
f.Content = []string{pact.ContentToolHeadings, pact.ContentToolEdit}
|
|
}
|
|
return []Finding{f}
|
|
}
|
|
return nil
|
|
}
|