@lynxflow/seo-engine 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -10393,6 +10393,662 @@ ${kwList}
10393
10393
  }
10394
10394
  }
10395
10395
 
10396
+ // src/search-intent-classifier.ts
10397
+ class SearchIntentClassifier {
10398
+ static INFORMATIONAL_SIGNALS = [
10399
+ "what",
10400
+ "why",
10401
+ "how",
10402
+ "when",
10403
+ "where",
10404
+ "who",
10405
+ "guide",
10406
+ "tutorial",
10407
+ "learn",
10408
+ "tips",
10409
+ "best practices",
10410
+ "explained",
10411
+ "definition",
10412
+ "meaning",
10413
+ "comment",
10414
+ "pourquoi",
10415
+ "qu'est ce que",
10416
+ "tutoriel",
10417
+ "guide",
10418
+ "astuces",
10419
+ "que es",
10420
+ "como",
10421
+ "guia",
10422
+ "was ist",
10423
+ "wie",
10424
+ "anleitung"
10425
+ ];
10426
+ static NAVIGATIONAL_SIGNALS = [
10427
+ "login",
10428
+ "sign in",
10429
+ "website",
10430
+ "official",
10431
+ "home page",
10432
+ "account",
10433
+ "dashboard",
10434
+ "portal",
10435
+ "app",
10436
+ "connexion",
10437
+ "se connecter",
10438
+ "portail",
10439
+ "iniciar sesion",
10440
+ "anmelden",
10441
+ "konto"
10442
+ ];
10443
+ static TRANSACTIONAL_SIGNALS = [
10444
+ "buy",
10445
+ "purchase",
10446
+ "order",
10447
+ "download",
10448
+ "get",
10449
+ "pricing",
10450
+ "cost",
10451
+ "free trial",
10452
+ "sign up",
10453
+ "subscribe",
10454
+ "install",
10455
+ "coupon",
10456
+ "deal",
10457
+ "discount",
10458
+ "cheap",
10459
+ "affordable",
10460
+ "acheter",
10461
+ "commander",
10462
+ "telecharger",
10463
+ "tarif",
10464
+ "prix",
10465
+ "essai gratuit",
10466
+ "inscription",
10467
+ "comprar",
10468
+ "precio",
10469
+ "kaufen",
10470
+ "preise"
10471
+ ];
10472
+ static COMMERCIAL_SIGNALS = [
10473
+ "best",
10474
+ "top",
10475
+ "review",
10476
+ "vs",
10477
+ "versus",
10478
+ "compare",
10479
+ "comparison",
10480
+ "alternative",
10481
+ "alternatives",
10482
+ "like",
10483
+ "similar",
10484
+ "better than",
10485
+ "instead of",
10486
+ "or",
10487
+ "option",
10488
+ "choice",
10489
+ "meilleur",
10490
+ "avis",
10491
+ "comparatif",
10492
+ "alternatives a",
10493
+ "mejor",
10494
+ "opiniones",
10495
+ "beste",
10496
+ "test",
10497
+ "vergleich"
10498
+ ];
10499
+ static classify(keyword, serpFeatures) {
10500
+ const clean = keyword.toLowerCase().trim();
10501
+ const scores = {
10502
+ informational: 0,
10503
+ navigational: 0,
10504
+ transactional: 0,
10505
+ commercial_investigation: 0
10506
+ };
10507
+ const detectedSignals = [];
10508
+ for (const signal of this.INFORMATIONAL_SIGNALS) {
10509
+ if (new RegExp(`\\b${signal}\\b`, "i").test(clean)) {
10510
+ scores.informational += 35;
10511
+ detectedSignals.push(`[info] ${signal}`);
10512
+ }
10513
+ }
10514
+ for (const signal of this.NAVIGATIONAL_SIGNALS) {
10515
+ if (new RegExp(`\\b${signal}\\b`, "i").test(clean)) {
10516
+ scores.navigational += 45;
10517
+ detectedSignals.push(`[nav] ${signal}`);
10518
+ }
10519
+ }
10520
+ for (const signal of this.TRANSACTIONAL_SIGNALS) {
10521
+ if (new RegExp(`\\b${signal}\\b`, "i").test(clean)) {
10522
+ scores.transactional += 40;
10523
+ detectedSignals.push(`[trans] ${signal}`);
10524
+ }
10525
+ }
10526
+ for (const signal of this.COMMERCIAL_SIGNALS) {
10527
+ if (new RegExp(`\\b${signal}\\b`, "i").test(clean)) {
10528
+ scores.commercial_investigation += 40;
10529
+ detectedSignals.push(`[comm] ${signal}`);
10530
+ }
10531
+ }
10532
+ if (serpFeatures && serpFeatures.length > 0) {
10533
+ for (const feature of serpFeatures) {
10534
+ const f = feature.toLowerCase();
10535
+ if (f.includes("shopping") || f.includes("local_pack") || f.includes("ads")) {
10536
+ scores.transactional += 25;
10537
+ } else if (f.includes("snippet") || f.includes("people_also_ask") || f.includes("knowledge")) {
10538
+ scores.informational += 25;
10539
+ } else if (f.includes("carousel") || f.includes("reviews")) {
10540
+ scores.commercial_investigation += 20;
10541
+ }
10542
+ }
10543
+ }
10544
+ let primaryIntent = "informational";
10545
+ let maxScore = scores.informational;
10546
+ if (scores.commercial_investigation > maxScore) {
10547
+ primaryIntent = "commercial_investigation";
10548
+ maxScore = scores.commercial_investigation;
10549
+ }
10550
+ if (scores.transactional > maxScore) {
10551
+ primaryIntent = "transactional";
10552
+ maxScore = scores.transactional;
10553
+ }
10554
+ if (scores.navigational > maxScore) {
10555
+ primaryIntent = "navigational";
10556
+ maxScore = scores.navigational;
10557
+ }
10558
+ if (maxScore === 0) {
10559
+ if (clean.split(" ").length <= 2) {
10560
+ primaryIntent = "navigational";
10561
+ } else {
10562
+ primaryIntent = "informational";
10563
+ }
10564
+ maxScore = 40;
10565
+ }
10566
+ const confidenceScore = Math.min(98, Math.max(45, maxScore));
10567
+ const formatMap = {
10568
+ informational: "Long-form Step-by-Step Guide, FAQ Accordion, Definition Card, or Tutorial Video.",
10569
+ commercial_investigation: "Comparison Matrix Table (VS), Tiered Review Breakdown, Pros/Cons List.",
10570
+ transactional: "High-Converting Landing Page, Pricing Calculator, Frictionless Signup Flow.",
10571
+ navigational: "Direct Brand Portal, Login Page, Dashboard Directory."
10572
+ };
10573
+ return {
10574
+ primaryIntent,
10575
+ confidenceScore,
10576
+ scores,
10577
+ detectedSignals,
10578
+ recommendedContentFormat: formatMap[primaryIntent]
10579
+ };
10580
+ }
10581
+ }
10582
+
10583
+ // src/seo-opportunity-prioritizer.ts
10584
+ class SeoOpportunityPrioritizer {
10585
+ static BENCHMARK_CTR = {
10586
+ 1: 0.316,
10587
+ 2: 0.157,
10588
+ 3: 0.105,
10589
+ 4: 0.075,
10590
+ 5: 0.059,
10591
+ 6: 0.048,
10592
+ 7: 0.041,
10593
+ 8: 0.035,
10594
+ 9: 0.031,
10595
+ 10: 0.027,
10596
+ 11: 0.018,
10597
+ 12: 0.015,
10598
+ 13: 0.013,
10599
+ 14: 0.012,
10600
+ 15: 0.011,
10601
+ 16: 0.01,
10602
+ 17: 0.009,
10603
+ 18: 0.008,
10604
+ 19: 0.008,
10605
+ 20: 0.007
10606
+ };
10607
+ static evaluate(input) {
10608
+ const pos = input.currentPosition ?? 25;
10609
+ const vol = input.searchVolume || input.monthlyImpressions || 500;
10610
+ const diff = input.difficulty ?? 40;
10611
+ let volumeScore = Math.min(100, Math.round(Math.log10(Math.max(10, vol)) / 4.5 * 100));
10612
+ let positionScore = 0;
10613
+ let archetype = "high_volume_gap";
10614
+ if (pos >= 11 && pos <= 20) {
10615
+ positionScore = 95;
10616
+ archetype = "quick_win";
10617
+ } else if (pos >= 4 && pos <= 10) {
10618
+ positionScore = 85;
10619
+ archetype = "top_3_push";
10620
+ } else if (pos >= 1 && pos <= 3) {
10621
+ positionScore = 40;
10622
+ } else {
10623
+ positionScore = Math.max(10, 70 - (pos - 20) * 1.5);
10624
+ }
10625
+ let intentScore = 60;
10626
+ if (input.intent === "transactional")
10627
+ intentScore = 100;
10628
+ else if (input.intent === "commercial_investigation")
10629
+ intentScore = 85;
10630
+ else if (input.intent === "informational")
10631
+ intentScore = 65;
10632
+ else if (input.intent === "navigational")
10633
+ intentScore = 40;
10634
+ const competitionScore = Math.max(10, 100 - diff);
10635
+ const expectedCtr = this.BENCHMARK_CTR[Math.min(20, Math.max(1, Math.round(pos)))] || 0.005;
10636
+ const actualCtr = input.actualCtr ?? (input.monthlyClicks || 0) / Math.max(1, input.monthlyImpressions || 1);
10637
+ const isUnderperforming = actualCtr < expectedCtr * 0.7;
10638
+ if (isUnderperforming && pos <= 10) {
10639
+ archetype = "underperformer";
10640
+ }
10641
+ const ctrImprovementScore = isUnderperforming ? 90 : 50;
10642
+ const opportunityScore = Math.round(volumeScore * 0.25 + positionScore * 0.25 + intentScore * 0.2 + competitionScore * 0.15 + ctrImprovementScore * 0.15);
10643
+ const targetClicks = Math.round(vol * 0.15);
10644
+ const currentClicks = input.monthlyClicks || Math.round(vol * (input.actualCtr || 0.01));
10645
+ const estimatedClickGain = Math.max(50, targetClicks - currentClicks);
10646
+ let priorityLevel = "low";
10647
+ if (opportunityScore >= 80)
10648
+ priorityLevel = "critical";
10649
+ else if (opportunityScore >= 65)
10650
+ priorityLevel = "high";
10651
+ else if (opportunityScore >= 50)
10652
+ priorityLevel = "medium";
10653
+ let actionPlan = "";
10654
+ if (archetype === "quick_win") {
10655
+ actionPlan = `Quick Win on Position #${pos} (~${estimatedClickGain} clicks/mo upside). Add 2 internal links with exact anchor text and update H2 subheadings to jump to Page 1.`;
10656
+ } else if (archetype === "underperformer") {
10657
+ actionPlan = `Underperforming CTR (${(actualCtr * 100).toFixed(1)}% vs expected ${(expectedCtr * 100).toFixed(1)}%). Rewrite Meta Title and Description with numbers and power words to boost clicks immediately.`;
10658
+ } else if (archetype === "top_3_push") {
10659
+ actionPlan = `Push from Position #${pos} to Top 3. Add rich Schema.org FAQPage and inject 1 AEO direct answer block.`;
10660
+ } else {
10661
+ actionPlan = `Expand topic cluster depth and build 3-5 high-authority backlinks to improve domain topical equity.`;
10662
+ }
10663
+ return {
10664
+ opportunityScore,
10665
+ archetype,
10666
+ estimatedClickGain,
10667
+ factors: {
10668
+ volumeScore,
10669
+ positionScore,
10670
+ intentScore,
10671
+ competitionScore,
10672
+ ctrImprovementScore
10673
+ },
10674
+ priorityLevel,
10675
+ actionPlan
10676
+ };
10677
+ }
10678
+ }
10679
+
10680
+ // src/trust-signals-extractor.ts
10681
+ class TrustSignalsExtractor {
10682
+ static SOCIAL_PROOF_PATTERNS = [
10683
+ /\b(\d{1,3}(?:[,\s]\d{3})*\+?)\s*(?:customers?|users?|businesses?|clients?|teams?|creators?|entreprises?|utilisateurs?)\b/gi,
10684
+ /(?:trusted|used|loved)\s+by\s+(\d{1,3}(?:[,\s]\d{3})*\+?)/gi,
10685
+ /\b(\d+(?:\.\d+)?[kmb])\+?\s*(?:users?|businesses?|downloads?)/gi
10686
+ ];
10687
+ static RESULT_PATTERNS = [
10688
+ /\b(\d+%\s*(?:increase|decrease|growth|improvement|reduction|croissance|gain|economie|économies))\b/gi,
10689
+ /\b(\d+x\s*(?:faster|more|growth|plus vite|plus rapide))\b/gi,
10690
+ /(?:\$|€|£)\s*(\d{1,3}(?:[,\s]\d{3})*(?:\.\d{2})?)\s*(?:saved|économisé|gagné)?/gi
10691
+ ];
10692
+ static RISK_REVERSAL_PATTERNS = [
10693
+ /\b(\d+[- ]days?\s+free\s+trial|free\s+trial|essai\s+gratuit(?:\s+de\s+\d+\s+jours)?)\b/gi,
10694
+ /\b(no\s+credit\s+card\s+required|sans\s+carte\s+bancaire|sans\s+engagement)\b/gi,
10695
+ /\b(cancel\s+anytime|résiliation\s+à\s+tout\s+moment|satisfait\s+ou\s+remboursé|money[- ]back\s+guarantee)\b/gi,
10696
+ /\b(100%\s+free\s+migration|migration\s+gratuite)\b/gi
10697
+ ];
10698
+ static AUTHORITY_PATTERNS = [
10699
+ /(?:featured|seen|mentioned)\s+(?:in|on)\s+([A-Za-z0-9\s,]+)/gi,
10700
+ /(?:vu\s+dans|mentionné\s+par)\s+([A-Za-z0-9\s,]+)/gi,
10701
+ /\b(award[- ]winning|best[- ]rated|élu\s+meilleur|noté\s+4\.\d\/5|rated\s+4\.\d\/5)\b/gi
10702
+ ];
10703
+ static extract(content) {
10704
+ if (!content || content.trim().length === 0) {
10705
+ return {
10706
+ totalSignalsCount: 0,
10707
+ testimonials: [],
10708
+ socialProofCounts: [],
10709
+ specificResults: [],
10710
+ riskReversals: [],
10711
+ authorityMentions: [],
10712
+ trustScore: 0,
10713
+ isSufficientForConversion: false
10714
+ };
10715
+ }
10716
+ const socialProofCounts = [];
10717
+ const specificResults = [];
10718
+ const riskReversals = [];
10719
+ const authorityMentions = [];
10720
+ const testimonials = [];
10721
+ for (const pattern of this.SOCIAL_PROOF_PATTERNS) {
10722
+ const matches = content.match(pattern);
10723
+ if (matches) {
10724
+ matches.forEach((m) => {
10725
+ if (!socialProofCounts.includes(m.trim()))
10726
+ socialProofCounts.push(m.trim());
10727
+ });
10728
+ }
10729
+ }
10730
+ for (const pattern of this.RESULT_PATTERNS) {
10731
+ const matches = content.match(pattern);
10732
+ if (matches) {
10733
+ matches.forEach((m) => {
10734
+ if (!specificResults.includes(m.trim()))
10735
+ specificResults.push(m.trim());
10736
+ });
10737
+ }
10738
+ }
10739
+ for (const pattern of this.RISK_REVERSAL_PATTERNS) {
10740
+ const matches = content.match(pattern);
10741
+ if (matches) {
10742
+ matches.forEach((m) => {
10743
+ if (!riskReversals.includes(m.trim()))
10744
+ riskReversals.push(m.trim());
10745
+ });
10746
+ }
10747
+ }
10748
+ for (const pattern of this.AUTHORITY_PATTERNS) {
10749
+ const matches = content.match(pattern);
10750
+ if (matches) {
10751
+ matches.forEach((m) => {
10752
+ if (!authorityMentions.includes(m.trim()))
10753
+ authorityMentions.push(m.trim());
10754
+ });
10755
+ }
10756
+ }
10757
+ const quoteRegex = /"([^"]{25,250})"\s*(?:—|-|by)\s*\*?([A-Z][a-z]+(?:\s+[A-Z]\.?)?)\*?/g;
10758
+ let quoteMatch;
10759
+ while ((quoteMatch = quoteRegex.exec(content)) !== null) {
10760
+ testimonials.push({
10761
+ quote: quoteMatch[1].trim(),
10762
+ author: quoteMatch[2]?.trim()
10763
+ });
10764
+ }
10765
+ const totalSignalsCount = socialProofCounts.length + specificResults.length + riskReversals.length + authorityMentions.length + testimonials.length;
10766
+ let trustScore = Math.min(100, totalSignalsCount * 18);
10767
+ if (testimonials.length > 0)
10768
+ trustScore = Math.min(100, trustScore + 15);
10769
+ if (riskReversals.length > 0)
10770
+ trustScore = Math.min(100, trustScore + 15);
10771
+ const isSufficientForConversion = trustScore >= 50;
10772
+ return {
10773
+ totalSignalsCount,
10774
+ testimonials,
10775
+ socialProofCounts,
10776
+ specificResults,
10777
+ riskReversals,
10778
+ authorityMentions,
10779
+ trustScore,
10780
+ isSufficientForConversion
10781
+ };
10782
+ }
10783
+ }
10784
+
10785
+ // src/competitor-gap-blueprint.ts
10786
+ class CompetitorGapBlueprintEngine {
10787
+ static buildBlueprint(targetTopic, userWordCount, competitors) {
10788
+ const gaps = [];
10789
+ const headingCounts = {};
10790
+ const outdatedItems = [];
10791
+ const diffAngles = [];
10792
+ for (const comp of competitors) {
10793
+ for (const h of comp.headings) {
10794
+ const cleanH = h.toLowerCase().trim();
10795
+ headingCounts[cleanH] = (headingCounts[cleanH] || 0) + 1;
10796
+ }
10797
+ if (comp.wordCount < 1500) {
10798
+ gaps.push({
10799
+ gapType: "thin_section",
10800
+ description: `Competitor article is surface-level (${comp.wordCount} words) and lacks concrete implementation steps.`,
10801
+ locationHeading: comp.headings[0] || "Overview",
10802
+ competitorUrl: comp.url,
10803
+ priority: "high",
10804
+ counterOpportunity: `Publish an exhaustive, deep-dive section with interactive code examples, tables, and verifiable benchmarks.`
10805
+ });
10806
+ }
10807
+ if (comp.contentSnippet) {
10808
+ if (/202[0-4]/i.test(comp.contentSnippet)) {
10809
+ outdatedItems.push(`Competitor ${comp.url} contains outdated 2020-2024 benchmarks and legacy screenshots.`);
10810
+ }
10811
+ }
10812
+ }
10813
+ const minThreshold = Math.max(1, Math.floor(competitors.length * 0.4));
10814
+ const expectedCommonHeadings = Object.entries(headingCounts).filter(([_, count]) => count >= minThreshold).map(([h]) => h.charAt(0).toUpperCase() + h.slice(1));
10815
+ diffAngles.push(`Real-Time Performance & In-Memory (O(1)) Speed Benchmarks (Zero competitors currently measure latency)`);
10816
+ diffAngles.push(`AI Engine Optimization (SearchGPT, Perplexity & Gemini citations) with structured direct answer cards`);
10817
+ diffAngles.push(`Interactive ROI / Cost Calculators embeddable directly in the content`);
10818
+ const winningOutlineRecommendation = [
10819
+ `1. Executive Summary & Direct Answer Card (For Google AI Overview & Perplexity)`,
10820
+ ...expectedCommonHeadings.map((h, i) => `${i + 2}. ${h} (With Real Case Studies & Data)`),
10821
+ `${expectedCommonHeadings.length + 2}. Proprietary Benchmark & Comparison Matrix (Our Unique Angle)`,
10822
+ `${expectedCommonHeadings.length + 3}. Step-by-Step Implementation Guide for 2026`,
10823
+ `${expectedCommonHeadings.length + 4}. Frequently Asked Questions (Structured JSON-LD FAQPage)`
10824
+ ];
10825
+ return {
10826
+ mustFillGaps: gaps,
10827
+ expectedCommonHeadings,
10828
+ differentiationAngles: diffAngles,
10829
+ outdatedToUpdate: outdatedItems,
10830
+ winningOutlineRecommendation
10831
+ };
10832
+ }
10833
+ }
10834
+
10835
+ // src/engagement-hook-analyzer.ts
10836
+ class EngagementHookAnalyzer {
10837
+ static GENERIC_BAD_OPENERS = [
10838
+ /^[A-Z][^.!?]*\bis\s+(?:a|an|the)\b/i,
10839
+ /^[A-Z][^.!?]*\bare\s+(?:a|an|the)\b/i,
10840
+ /^When it comes to/i,
10841
+ /^In (?:today['’]s|the|this)\s+(?:digital|modern|world|age|fast-paced)/i,
10842
+ /^If you['’]re (?:looking|searching|trying)/i,
10843
+ /^(?:Many|Most|Some)\s+(?:people|companies|businesses|founders)/i,
10844
+ /^There are (?:many|several|numerous)/i,
10845
+ /^It['’]s (?:no secret|important|essential)/i,
10846
+ /^De nos jours/i,
10847
+ /^Dans le monde digital d'aujourd'hui/i
10848
+ ];
10849
+ static GOOD_HOOK_STARTERS = [
10850
+ { type: "question", pattern: /\?$/ },
10851
+ { type: "statistic", pattern: /^(?:\d+%|\d{1,3}(?:,\d{3})*|\$\d+)/ },
10852
+ { type: "quote", pattern: /^["'«]/ },
10853
+ { type: "thought_experiment", pattern: /^(?:What if|Imagine|Picture this|Here['’]s (?:the truth|what happened)|Et si|Imaginez)/i },
10854
+ { type: "concrete_story", pattern: /^(?:Last (?:week|month|year)|In \d{4}|Hier|Le mois dernier|[A-Z][a-z]+ spent \d+)/i }
10855
+ ];
10856
+ static CTA_REGEX = /(?:Start|Try|Get|Claim|Download|Sign up|Essayer|Démarrer|Obtenir|Profiter|Découvrir|\[.*?→\]|\*\*\[.*?\]\*\*)/gi;
10857
+ static analyze(content) {
10858
+ const lines = content.split(`
10859
+ `).map((l) => l.trim()).filter(Boolean);
10860
+ const textOnlyLines = lines.filter((l) => !l.startsWith("#") && !l.startsWith("---") && !l.startsWith("**Meta"));
10861
+ const firstParagraph = textOnlyLines[0] || "";
10862
+ const firstSentence = firstParagraph.split(/[.?!]/)[0]?.trim() || "";
10863
+ const fixes = [];
10864
+ let isStrongHook = false;
10865
+ let hookType = "generic_bad_opener";
10866
+ let hookFeedback = "Opening is generic. Replace with a punchy question, surprising stat, or narrative scenario.";
10867
+ const isGeneric = this.GENERIC_BAD_OPENERS.some((r) => r.test(firstSentence));
10868
+ if (!isGeneric) {
10869
+ for (const h of this.GOOD_HOOK_STARTERS) {
10870
+ if (h.pattern.test(firstSentence) || h.pattern.test(firstParagraph)) {
10871
+ isStrongHook = true;
10872
+ hookType = h.type;
10873
+ hookFeedback = `Excellent hook using a ${h.type.replace("_", " ")}. Captures attention instantly.`;
10874
+ break;
10875
+ }
10876
+ }
10877
+ if (!isStrongHook) {
10878
+ isStrongHook = true;
10879
+ hookType = "concrete_story";
10880
+ hookFeedback = "Opening avoids cliché LLM patterns.";
10881
+ }
10882
+ } else {
10883
+ fixes.push(`Replace generic opener ("${firstSentence.slice(0, 45)}...") with a dramatic question or verified statistic.`);
10884
+ }
10885
+ const hook = {
10886
+ openingSentence: firstSentence,
10887
+ isStrongHook,
10888
+ hookType,
10889
+ feedback: hookFeedback,
10890
+ suggestedHookUpgrade: `What if you could eliminate 80% of manual repetitive tasks in less than 48 hours?`
10891
+ };
10892
+ const rawSentences = content.replace(/([.?!])\s*(?=[A-Z0-9])/g, "$1|").split("|").map((s) => s.trim()).filter((s) => s.length > 5);
10893
+ const sentenceLengths = rawSentences.map((s) => s.split(/\s+/).filter(Boolean).length);
10894
+ const sCount = Math.max(1, sentenceLengths.length);
10895
+ const avgWords = Math.round(sentenceLengths.reduce((a, b) => a + b, 0) / sCount * 10) / 10;
10896
+ const variance = sentenceLengths.reduce((acc, len) => acc + Math.pow(len - avgWords, 2), 0) / sCount;
10897
+ const stdDev = Math.round(Math.sqrt(variance) * 10) / 10;
10898
+ const hasVariedRhythm = stdDev >= 5 && avgWords <= 18;
10899
+ const rhythmScore = Math.min(100, Math.round(stdDev / 8 * 100));
10900
+ if (!hasVariedRhythm) {
10901
+ fixes.push("Vary sentence lengths: Alternate short punchy statements (4-7 words) with explanatory sentences (14-18 words).");
10902
+ }
10903
+ const rhythm = {
10904
+ sentenceCount: sCount,
10905
+ averageWords: avgWords,
10906
+ standardDeviation: stdDev,
10907
+ hasVariedRhythm,
10908
+ score: rhythmScore
10909
+ };
10910
+ const thirdOfText = Math.floor(content.length / 3);
10911
+ const topPart = content.slice(0, thirdOfText);
10912
+ const midPart = content.slice(thirdOfText, thirdOfText * 2);
10913
+ const botPart = content.slice(thirdOfText * 2);
10914
+ const hasAboveTheFoldCta = this.CTA_REGEX.test(topPart);
10915
+ const hasMidArticleCta = this.CTA_REGEX.test(midPart);
10916
+ const hasBottomCta = this.CTA_REGEX.test(botPart);
10917
+ const allCtas = content.match(this.CTA_REGEX) || [];
10918
+ const isDistributed = (hasAboveTheFoldCta || hasMidArticleCta) && hasBottomCta;
10919
+ if (!isDistributed) {
10920
+ fixes.push("Add a mid-article contextual CTA box to capture readers who don't reach the bottom footer.");
10921
+ }
10922
+ const ctas = {
10923
+ totalCtasDetected: allCtas.length,
10924
+ isDistributedAcrossArticle: isDistributed,
10925
+ hasAboveTheFoldCta,
10926
+ hasMidArticleCta,
10927
+ hasBottomCta
10928
+ };
10929
+ const paragraphs = content.split(/\n\s*\n/).filter((p) => p.trim().length > 20 && !p.startsWith("#"));
10930
+ let longParas = 0;
10931
+ for (const p of paragraphs) {
10932
+ const pSentences = p.split(/[.?!]/).filter((s) => s.trim().length > 3).length;
10933
+ if (pSentences > 4)
10934
+ longParas++;
10935
+ }
10936
+ const isMobileBreathable = longParas <= Math.max(1, Math.floor(paragraphs.length * 0.15));
10937
+ if (!isMobileBreathable) {
10938
+ fixes.push(`Split ${longParas} bulky paragraphs into 2-3 sentence chunks to improve mobile scannability.`);
10939
+ }
10940
+ const paragraphEval = {
10941
+ totalParagraphs: paragraphs.length,
10942
+ longParagraphsCount: longParas,
10943
+ isMobileBreathable
10944
+ };
10945
+ const passedArray = [isStrongHook, hasVariedRhythm, isDistributed, isMobileBreathable];
10946
+ const passedCount = passedArray.filter(Boolean).length;
10947
+ const engagementScore = Math.round(passedCount / 4 * 100);
10948
+ return {
10949
+ engagementScore,
10950
+ allPassed: passedCount === 4,
10951
+ passedCount,
10952
+ hook,
10953
+ rhythm,
10954
+ ctas,
10955
+ paragraphs: paragraphEval,
10956
+ actionableFixes: fixes
10957
+ };
10958
+ }
10959
+ }
10960
+
10961
+ // src/keyword-density-guard.ts
10962
+ class KeywordDensityGuard {
10963
+ static audit(content, primaryKeyword, metaTitle) {
10964
+ if (!content || !primaryKeyword) {
10965
+ return {
10966
+ keyword: primaryKeyword,
10967
+ totalOccurrences: 0,
10968
+ totalWords: 0,
10969
+ densityPercent: 0,
10970
+ status: "too_low",
10971
+ placements: [],
10972
+ consecutiveSentencesWithKeyword: 0,
10973
+ stuffingAlert: false,
10974
+ recommendations: ["Provide content and a target keyword to audit."]
10975
+ };
10976
+ }
10977
+ const words = content.split(/\s+/).filter(Boolean);
10978
+ const totalWords = words.length;
10979
+ const escapedKw = primaryKeyword.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
10980
+ const kwRegex = new RegExp(`\\b${escapedKw}\\b`, "gi");
10981
+ const testKw = (str) => new RegExp(`\\b${escapedKw}\\b`, "i").test(str);
10982
+ const occurrences = (content.match(kwRegex) || []).length;
10983
+ const densityPercent = totalWords > 0 ? Math.round(occurrences / totalWords * 1000) / 10 : 0;
10984
+ const lines = content.split(`
10985
+ `);
10986
+ const h1Line = lines.find((l) => l.trim().startsWith("# ")) || "";
10987
+ const h2Lines = lines.filter((l) => l.trim().startsWith("## ")).join(" ");
10988
+ const first100Words = words.slice(0, 100).join(" ");
10989
+ const last100Words = words.slice(-100).join(" ");
10990
+ const inTitle = metaTitle ? testKw(metaTitle) : false;
10991
+ const inH1 = testKw(h1Line);
10992
+ const inFirst100 = testKw(first100Words);
10993
+ const inH2s = (h2Lines.match(new RegExp(`\\b${escapedKw}\\b`, "gi")) || []).length;
10994
+ const inConclusion = testKw(last100Words);
10995
+ const inFaq = /faq|frequently asked/i.test(content) && testKw(content);
10996
+ const placements = [
10997
+ { location: "title", present: inTitle, count: inTitle ? 1 : 0 },
10998
+ { location: "h1", present: inH1, count: inH1 ? 1 : 0 },
10999
+ { location: "first_100_words", present: inFirst100, count: inFirst100 ? 1 : 0 },
11000
+ { location: "h2_headings", present: inH2s > 0, count: inH2s },
11001
+ { location: "conclusion", present: inConclusion, count: inConclusion ? 1 : 0 },
11002
+ { location: "faq", present: inFaq, count: inFaq ? 1 : 0 }
11003
+ ];
11004
+ const sentences = content.split(/[.?!]/).map((s) => s.trim()).filter((s) => s.length > 5);
11005
+ let maxConsecutive = 0;
11006
+ let currentConsecutive = 0;
11007
+ for (const s of sentences) {
11008
+ if (kwRegex.test(s)) {
11009
+ currentConsecutive++;
11010
+ if (currentConsecutive > maxConsecutive)
11011
+ maxConsecutive = currentConsecutive;
11012
+ } else {
11013
+ currentConsecutive = 0;
11014
+ }
11015
+ }
11016
+ const stuffingAlert = densityPercent > 2.2 || maxConsecutive >= 3;
11017
+ let status = "optimal";
11018
+ const recommendations = [];
11019
+ if (densityPercent < 0.8) {
11020
+ status = "too_low";
11021
+ recommendations.push(`Primary keyword density is low (${densityPercent}%). Target: 1.0% - 1.8%.`);
11022
+ } else if (densityPercent > 2.2) {
11023
+ status = "stuffing_risk";
11024
+ recommendations.push(`Keyword stuffing risk detected (${densityPercent}%). Reduce usage and replace with semantic LSI synonyms.`);
11025
+ }
11026
+ if (!inFirst100) {
11027
+ recommendations.push("Include primary keyword naturally in the first 100 words.");
11028
+ }
11029
+ if (!inH1) {
11030
+ recommendations.push("Include primary keyword in the main H1 heading.");
11031
+ }
11032
+ if (inH2s === 0) {
11033
+ recommendations.push("Include primary keyword in at least one H2 subheading.");
11034
+ }
11035
+ if (maxConsecutive >= 3) {
11036
+ recommendations.push(`Keyword appears in ${maxConsecutive} consecutive sentences. Space out mentions to improve natural reading flow.`);
11037
+ }
11038
+ return {
11039
+ keyword: primaryKeyword,
11040
+ totalOccurrences: occurrences,
11041
+ totalWords,
11042
+ densityPercent,
11043
+ status,
11044
+ placements,
11045
+ consecutiveSentencesWithKeyword: maxConsecutive,
11046
+ stuffingAlert,
11047
+ recommendations
11048
+ };
11049
+ }
11050
+ }
11051
+
10396
11052
  // src/index.ts
10397
11053
  function createLynxSeoEngine(config) {
10398
11054
  return new LynxSeoEngine(config);
@@ -10412,6 +11068,12 @@ var LynxSeo = {
10412
11068
  readabilityComplexity: PassiveVoiceComplexityAnalyzer,
10413
11069
  aboveFoldCro: AboveFoldCroAuditor,
10414
11070
  contextTemplates: ContextTemplatesGenerator,
11071
+ searchIntent: SearchIntentClassifier,
11072
+ opportunityPrioritizer: SeoOpportunityPrioritizer,
11073
+ trustSignals: TrustSignalsExtractor,
11074
+ competitorGap: CompetitorGapBlueprintEngine,
11075
+ engagementHook: EngagementHookAnalyzer,
11076
+ keywordDensity: KeywordDensityGuard,
10415
11077
  inspectMeta: SiteAuditor.inspectMeta,
10416
11078
  crawlDomain: DeepCrawlerAuditor.crawlAndAuditDomain,
10417
11079
  inspectHtmlSnapshot: DeepCrawlerAuditor.inspectHtmlSnapshot,
@@ -10471,6 +11133,7 @@ export {
10471
11133
  VideoYouTubeAnalyzer,
10472
11134
  UrlyticsEngine,
10473
11135
  UI_ICONS,
11136
+ TrustSignalsExtractor,
10474
11137
  TokenQuotaManager,
10475
11138
  TechnicalRulesAuditor,
10476
11139
  TeamRbacEngine,
@@ -10482,8 +11145,10 @@ export {
10482
11145
  SerpRankHistoryEngine,
10483
11146
  SerpLengthComparator,
10484
11147
  SerpClient,
11148
+ SeoOpportunityPrioritizer,
10485
11149
  SeoOpportunitiesDecayDetector,
10486
11150
  SemanticCannibalizationDetector,
11151
+ SearchIntentClassifier,
10487
11152
  SchemaGraphBuilder,
10488
11153
  SUPPORTED_CANONICAL_LOCALES,
10489
11154
  SCHEMA_LOCAL_BUSINESS_MAP,
@@ -10516,6 +11181,7 @@ export {
10516
11181
  KnowledgeGraphLinker,
10517
11182
  KnowledgeBankBuilder,
10518
11183
  KeywordPermutatorEngine,
11184
+ KeywordDensityGuard,
10519
11185
  IsrCacheManager,
10520
11186
  InternalPageRankEngine,
10521
11187
  InstantMatrixSearchEngine,
@@ -10531,12 +11197,14 @@ export {
10531
11197
  FeaturesKnowledgeHarvester,
10532
11198
  ExtendedSchemaGraphBuilder,
10533
11199
  ExistingMediaHarvester,
11200
+ EngagementHookAnalyzer,
10534
11201
  EmbeddableSeoWidgetGenerator,
10535
11202
  DeepCrawlerAuditor,
10536
11203
  CrosslinkScorerEngine,
10537
11204
  CroCopywritingEngine,
10538
11205
  CopywritingFrameworksMaster,
10539
11206
  ContextTemplatesGenerator,
11207
+ CompetitorGapBlueprintEngine,
10540
11208
  CONTENT_AI_40_TOOLS,
10541
11209
  BrandDnaCalendarEngine,
10542
11210
  BacklinksClient,