|
# retoor <retoor@molodetz.nl>
|
|
|
|
from __future__ import annotations
|
|
|
|
from .base import (
|
|
MEDIUM,
|
|
LOW,
|
|
WARN,
|
|
Check,
|
|
PageContext,
|
|
SiteContext,
|
|
ok_check,
|
|
page_check,
|
|
)
|
|
|
|
CATEGORY = "content"
|
|
|
|
THIN_CONTENT_WORDS = 250
|
|
|
|
|
|
@page_check
|
|
def single_h1(page: PageContext, site: SiteContext) -> list:
|
|
h1s = page.dom.get("h1", []) or []
|
|
results = [
|
|
ok_check(
|
|
"content.h1_present",
|
|
CATEGORY,
|
|
"H1 heading",
|
|
len(h1s) >= 1,
|
|
severity=MEDIUM,
|
|
value=(h1s[0][:80] if h1s else "missing"),
|
|
recommendation="Add a single descriptive H1 heading.",
|
|
url=page.url,
|
|
)
|
|
]
|
|
if len(h1s) > 1:
|
|
results.append(
|
|
Check(
|
|
"content.h1_single",
|
|
CATEGORY,
|
|
"Single H1",
|
|
WARN,
|
|
LOW,
|
|
value=f"{len(h1s)} H1s",
|
|
recommendation="Use exactly one H1 per page.",
|
|
url=page.url,
|
|
)
|
|
)
|
|
return results
|
|
|
|
|
|
@page_check
|
|
def heading_order(page: PageContext, site: SiteContext) -> Check:
|
|
headings = page.dom.get("headings", []) or []
|
|
levels = [int(h.get("level", 0)) for h in headings if h.get("level")]
|
|
skipped = False
|
|
prev = 0
|
|
for level in levels:
|
|
if prev and level > prev + 1:
|
|
skipped = True
|
|
break
|
|
prev = level
|
|
return ok_check(
|
|
"content.heading_order",
|
|
CATEGORY,
|
|
"Heading hierarchy",
|
|
not skipped,
|
|
severity=LOW,
|
|
value="ordered" if not skipped else "skips a level",
|
|
recommendation="Do not skip heading levels (e.g. H2 to H4).",
|
|
warn=True,
|
|
url=page.url,
|
|
)
|
|
|
|
|
|
@page_check
|
|
def word_count(page: PageContext, site: SiteContext) -> Check:
|
|
words = int(page.dom.get("wordCount", 0) or 0)
|
|
passed = words >= THIN_CONTENT_WORDS
|
|
return ok_check(
|
|
"content.word_count",
|
|
CATEGORY,
|
|
"Content depth",
|
|
passed,
|
|
severity=MEDIUM,
|
|
value=f"{words} words",
|
|
recommendation=f"Thin content; aim for more than {THIN_CONTENT_WORDS} meaningful words.",
|
|
warn=True,
|
|
url=page.url,
|
|
)
|