""" Conversion Optimization rules — detect patterns that hurt business outcomes regardless of visual polish: weak CTAs, excessive form friction, missing trust signals, unclear hero messaging, and absent pricing clarity. """ from __future__ import annotations from backend.models.issue import Category, Issue, Severity _CAT = Category.CONVERSION_OPTIMIZATION _WEAK_CTA_PHRASES = frozenset({ "click here", "click", "here", "submit", "learn more", "read more", "more", "go", "ok", "button", "see more", "view more", "find out", "find out more", "enter", }) _ACTION_VERBS = frozenset({ "start", "get", "try", "sign", "join", "create", "build", "launch", "download", "buy", "order", "book", "schedule", "request", "apply", "explore", "discover", "watch", "play", "upgrade", "activate", }) def analyze(parsed_page: dict, thresholds: dict) -> list[Issue]: t = thresholds.get("conversion", {}) issues: list[Issue] = [] _check_weak_cta(parsed_page, t, issues) _check_form_friction(parsed_page, t, issues) _check_trust_signals(parsed_page, t, issues) _check_hero_clarity(parsed_page, t, issues) _check_pricing_cta(parsed_page, issues) return issues # ── CV1 — weak CTA text ─────────────────────────────────────────────────────── def _check_weak_cta(parsed_page: dict, t: dict, issues: list[Issue]) -> None: cta_texts = parsed_page.get("cta_texts", []) if not cta_texts: return weak = [ txt for txt in cta_texts if txt.lower().strip() in _WEAK_CTA_PHRASES ] # Check if ANY CTA uses a strong action verb has_strong = any( any(verb in txt.lower() for verb in _ACTION_VERBS) for txt in cta_texts ) if weak and not has_strong: issues.append(Issue( rule_id="CV1_weak_cta_text", category=_CAT, severity=Severity.HIGH, confidence=0.80, message=( f"{len(weak)} CTA(s) use generic text " f"({', '.join(repr(w) for w in weak[:3])}) with no strong action verb found." ), recommendation=( "Replace passive CTA text with outcome-oriented action verbs: " "'Start Free Trial', 'Get My Report', 'Join 10,000 Users'. " "Specific CTAs outperform generic ones by 90% on average." ), evidence=f"weak_ctas={weak[:5]}", estimated_time="15 minutes", why=( "CTA copy is the last thing a user reads before deciding to convert. " "Generic phrases like 'Submit' or 'Learn More' create uncertainty — " "the user doesn't know what happens next. Specific action verbs set clear " "expectations and increase click-through rates by 20–90% in A/B tests." ), references=["Copyhackers", "HubSpot CTA Research", "Nielsen Norman Group"], )) elif weak: issues.append(Issue( rule_id="CV1_weak_cta_text", category=_CAT, severity=Severity.MEDIUM, confidence=0.75, message=f"{len(weak)} CTA(s) use generic text: {', '.join(repr(w) for w in weak[:3])}.", recommendation=( "Audit each generic CTA and rewrite with the user's desired outcome: " "'Get Access', 'Download the Guide', 'See Pricing'." ), evidence=f"weak_ctas={weak[:5]}", estimated_time="20 minutes", why=( "Even when strong CTAs exist on the page, weak CTAs in secondary positions " "reduce overall conversion confidence. Every button should feel intentional." ), references=["Copyhackers", "ConversionXL"], )) # ── CV2 — form friction ─────────────────────────────────────────────────────── def _check_form_friction(parsed_page: dict, t: dict, issues: list[Issue]) -> None: max_fields = t.get("max_form_fields", 6) counts = parsed_page.get("form_field_counts", []) overloaded = [c for c in counts if c > max_fields] if overloaded: worst = max(overloaded) issues.append(Issue( rule_id="CV2_form_friction", category=_CAT, severity=Severity.HIGH, confidence=0.85, message=( f"{len(overloaded)} form(s) have more than {max_fields} fields " f"(worst: {worst} fields) — high friction reduces sign-ups by 50%+." ), recommendation=( f"Reduce to {max_fields} or fewer fields for initial sign-up/checkout. " "Defer optional fields (phone, company size) to onboarding or profile setup. " "Use progressive disclosure for multi-step flows." ), evidence=f"form_field_counts={counts}", estimated_time="2 hours", why=( "Every additional field in a sign-up form reduces conversion rate by ~4–10%. " "Experian found that reducing from 11 to 4 fields increased conversions 120%. " "Collect only what's needed to get the user started — gather the rest later." ), references=["Experian Form Study", "Baymard Institute", "HubSpot Blog"], )) # ── CV3 — missing trust signals ─────────────────────────────────────────────── def _check_trust_signals(parsed_page: dict, t: dict, issues: list[Issue]) -> None: min_signals = t.get("min_trust_signals", 1) count = parsed_page.get("trust_signal_count", 0) has_testimonials = parsed_page.get("has_testimonials", False) if count < min_signals and not has_testimonials: issues.append(Issue( rule_id="CV3_missing_trust_signals", category=_CAT, severity=Severity.MEDIUM, confidence=0.70, message=( "No trust signals detected — no testimonials, security badges, " "review counts, or social proof elements found." ), recommendation=( "Add near every primary CTA: customer count ('10,000+ users'), " "a testimonial quote with attribution, star ratings, " "security badges (SSL, SOC2), or recognisable logos of customers/partners." ), evidence=f"trust_signal_count={count}, has_testimonials={has_testimonials}", estimated_time="3 hours", why=( "Trust signals reduce purchase anxiety. 88% of consumers trust online " "reviews as much as personal recommendations (BrightLocal). " "A single testimonial near a CTA can increase conversion by 34% " "(Trustpilot case studies). First-time visitors need social proof " "before committing — without it, they leave to search for reviews elsewhere." ), references=["BrightLocal Consumer Review Survey", "Trustpilot", "Nielsen Trust Study"], )) # ── CV4 — hero clarity ──────────────────────────────────────────────────────── def _check_hero_clarity(parsed_page: dict, t: dict, issues: list[Issue]) -> None: max_words = t.get("hero_max_words", 150) has_hero = parsed_page.get("has_hero", False) word_count = parsed_page.get("hero_word_count", 0) headings = parsed_page.get("headings", []) has_h1 = any(h["level"] == 1 for h in headings) if not has_h1: issues.append(Issue( rule_id="CV4_missing_value_proposition", category=_CAT, severity=Severity.HIGH, confidence=0.90, message="No