The route report takes an optional noindex list of static paths. When the key is present the stored set (golem15_seo_noindex_paths) is replaced, and the sitemap filter leaves those paths out with the reason noindex; a changed set asks the sitemap for a debounced rebuild through contract.Rebuilder. A listed noindex address without a CMS record now names sitemap_update_settings, or fix: code without the sitemap plugin.
151 lines
5.1 KiB
Go
151 lines
5.1 KiB
Go
package crawl
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
|
"git.golem15.com/golem15/sm-seo-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
// The SEO tools an issue may name (pier prefixes them with seo_).
|
|
const (
|
|
ToolSetURLSEO = "seo_set_url_seo"
|
|
ToolUpdateRobots = "seo_update_robots"
|
|
ToolCreateRedirect = "seo_create_redirect"
|
|
ToolUpdatePage = "seo_update_page"
|
|
ToolCreatePage = "seo_create_page"
|
|
ToolUpdateSettings = "seo_update_settings"
|
|
// ToolSitemapSettings is the sitemap plugin's settings tool, which
|
|
// edits its Additional addresses.
|
|
ToolSitemapSettings = "sitemap_update_settings"
|
|
)
|
|
|
|
// Record is the CMS record behind a crawled page: its content type, id
|
|
// and locale, the descriptor facts the checks compare the page with
|
|
// (title, word count, headings, images, the highest heading level) and the
|
|
// fixing tools its type names (pact.ContentDescriptor.Tools).
|
|
type Record struct {
|
|
Type string
|
|
ID string
|
|
Locale string
|
|
Title string
|
|
Words *int
|
|
Headings []pact.ContentHeading
|
|
Images []pact.ContentImage
|
|
TopHeading int
|
|
// Links are the record's CMS-known links: the address of its primary
|
|
// parent, which the page's breadcrumb trail links to.
|
|
Links []string
|
|
Tools map[string]string
|
|
// CustomJSONLD is true when the record's SEO tab stores custom JSON-LD.
|
|
CustomJSONLD bool
|
|
}
|
|
|
|
// tool returns the record's tool for the first operation of ops it
|
|
// names, "" when it names none.
|
|
func (r *Record) tool(ops ...string) string {
|
|
if r == nil {
|
|
return ""
|
|
}
|
|
for _, op := range ops {
|
|
if t := strings.TrimSpace(r.Tools[op]); t != "" {
|
|
return t
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// hasHeading reports whether the record's own headings hold one of level
|
|
// (any level when level is 0) whose text is text (any text when empty).
|
|
func (r *Record) hasHeading(level int, text string) bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
for _, h := range r.Headings {
|
|
if level != 0 && h.Level != level {
|
|
continue
|
|
}
|
|
if text == "" || normal(h.Text) == normal(text) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Finding is one problem a check found on a page, before it is stored:
|
|
// the kind, the address and locale, the details of the kind, and how it
|
|
// is fixed (P33-38). Tool is a fixed CMS tool (seo_*); Content lists the
|
|
// record operations (pact.ContentTool*) that fix a finding inside the
|
|
// record's own content, tried in order; What names the template element
|
|
// when it is fixed in code.
|
|
type Finding struct {
|
|
Kind string
|
|
URL string
|
|
Locale string
|
|
Details map[string]any
|
|
Tool string
|
|
Content []string
|
|
Record *Record
|
|
What string
|
|
// Check tells the two content_js findings apart (cms, rendered).
|
|
Check string
|
|
}
|
|
|
|
// Route applies P33-38: a finding inside the record's own content names
|
|
// the record type's fitting tool (H1 findings prefer Tools[headings],
|
|
// other content findings Tools[edit]); else a fixed CMS tool; else fix
|
|
// code with the template element in details.what. Every finding gets one
|
|
// of the two.
|
|
func Route(f Finding) (fix, tool, what string) {
|
|
if len(f.Content) > 0 {
|
|
if t := f.Record.tool(f.Content...); t != "" {
|
|
return models.FixTool, t, ""
|
|
}
|
|
}
|
|
if f.Tool != "" {
|
|
return models.FixTool, f.Tool, ""
|
|
}
|
|
what = f.What
|
|
if what == "" {
|
|
what = defaultWhat[f.Kind]
|
|
}
|
|
if what == "" {
|
|
what = "the page template"
|
|
}
|
|
return models.FixCode, "", what
|
|
}
|
|
|
|
// defaultWhat names the template element of a kind fixed in code.
|
|
var defaultWhat = map[string]string{
|
|
KindCanonicalMissing: `<link rel="canonical"> in the page head`,
|
|
KindH1Missing: "the <h1> of the page template",
|
|
KindH1Multiple: "the <h1> headings of the page template",
|
|
KindHeadingSkipped: "the heading levels of the page template",
|
|
KindHeadingEmpty: "the empty heading of the page template",
|
|
KindJSONLDInvalid: `the <script type="application/ld+json"> of the page template`,
|
|
KindJSONLDMultiple: `the <script type="application/ld+json"> blocks of the page template`,
|
|
KindHreflangRecip: `the <link rel="alternate" hreflang> links of the page template`,
|
|
KindImageAlt: "the alt attribute of the template's <img>",
|
|
KindImageSize: "the width and height attributes of the template's <img>",
|
|
KindFeaturedLazy: `loading="lazy" on the main image`,
|
|
KindContentJS: "server-side rendering of the page content",
|
|
KindGenericAnchor: "the link text in the page template",
|
|
KindRedirectedLink: "the link in the page template",
|
|
KindOrphanPage: "a link to this page from the site navigation or another page",
|
|
KindSitemapNoindex: "the address list of the sitemap that names this page",
|
|
}
|
|
|
|
// Fingerprint identifies a finding across crawls: the kind, the address,
|
|
// the locale and the check (content_js), so an ignore survives later
|
|
// crawls (P33-37).
|
|
func Fingerprint(kind, url, locale, check string) string {
|
|
sum := sha256.Sum256([]byte(kind + "\x00" + url + "\x00" + locale + "\x00" + check))
|
|
return hex.EncodeToString(sum[:16])
|
|
}
|
|
|
|
// normal is text compared without case and extra whitespace.
|
|
func normal(s string) string { return classes.NormalText(s) }
|