@lynxflow/seo-engine 2.4.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/README.md CHANGED
@@ -4,7 +4,7 @@ High-Performance Universal Programmatic SEO & Structured Data Engine for Modern
4
4
 
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
6
6
  [![License](https://img.shields.io/badge/License-Proprietary-green.svg)](LICENSE)
7
- [![Version](https://img.shields.io/badge/Version-2.4.0-orange.svg)](https://www.npmjs.com/package/@lynxflow/seo-engine)
7
+ [![Version](https://img.shields.io/badge/Version-2.5.0-orange.svg)](https://www.npmjs.com/package/@lynxflow/seo-engine)
8
8
 
9
9
  ---
10
10
 
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 🥊 Competitor Gap Blueprint & "Beat Them" Content Architect
3
+ * Ported & optimized from Python competitor_gap_analyzer.py.
4
+ * Identifies thin sections, unsupported claims, missing perspectives, and outdated information in competitor content.
5
+ */
6
+ export type ContentGapType = "thin_section" | "unsupported_claim" | "missing_perspective" | "outdated_info" | "structural_gap";
7
+ export interface CompetitorContentGap {
8
+ gapType: ContentGapType;
9
+ description: string;
10
+ locationHeading: string;
11
+ competitorUrl: string;
12
+ priority: "high" | "medium" | "low";
13
+ counterOpportunity: string;
14
+ }
15
+ export interface CompetitorArticleInput {
16
+ url: string;
17
+ title: string;
18
+ wordCount: number;
19
+ headings: string[];
20
+ contentSnippet?: string;
21
+ }
22
+ export interface BeatThemBlueprint {
23
+ mustFillGaps: CompetitorContentGap[];
24
+ expectedCommonHeadings: string[];
25
+ differentiationAngles: string[];
26
+ outdatedToUpdate: string[];
27
+ winningOutlineRecommendation: string[];
28
+ }
29
+ export declare class CompetitorGapBlueprintEngine {
30
+ /**
31
+ * Analyzes competitors and builds an actionable "Beat Them" blueprint.
32
+ */
33
+ static buildBlueprint(targetTopic: string, userWordCount: number, competitors: CompetitorArticleInput[]): BeatThemBlueprint;
34
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 🪝 Engagement Hook & Viral Reading Rhythm Analyzer
3
+ * Ported & optimized from Python engagement_analyzer.py.
4
+ * Evaluates the 4 viral reading criteria: Hook quality, sentence length variety, contextual CTA distribution, and paragraph breathability.
5
+ */
6
+ export interface HookEvaluation {
7
+ openingSentence: string;
8
+ isStrongHook: boolean;
9
+ hookType: "question" | "statistic" | "quote" | "thought_experiment" | "concrete_story" | "generic_bad_opener";
10
+ feedback: string;
11
+ suggestedHookUpgrade?: string;
12
+ }
13
+ export interface RhythmEvaluation {
14
+ sentenceCount: number;
15
+ averageWords: number;
16
+ standardDeviation: number;
17
+ hasVariedRhythm: boolean;
18
+ score: number;
19
+ }
20
+ export interface CtaDistributionEvaluation {
21
+ totalCtasDetected: number;
22
+ isDistributedAcrossArticle: boolean;
23
+ hasAboveTheFoldCta: boolean;
24
+ hasMidArticleCta: boolean;
25
+ hasBottomCta: boolean;
26
+ }
27
+ export interface ParagraphDensityEvaluation {
28
+ totalParagraphs: number;
29
+ longParagraphsCount: number;
30
+ isMobileBreathable: boolean;
31
+ }
32
+ export interface ArticleEngagementReport {
33
+ engagementScore: number;
34
+ allPassed: boolean;
35
+ passedCount: number;
36
+ hook: HookEvaluation;
37
+ rhythm: RhythmEvaluation;
38
+ ctas: CtaDistributionEvaluation;
39
+ paragraphs: ParagraphDensityEvaluation;
40
+ actionableFixes: string[];
41
+ }
42
+ export declare class EngagementHookAnalyzer {
43
+ private static readonly GENERIC_BAD_OPENERS;
44
+ private static readonly GOOD_HOOK_STARTERS;
45
+ private static readonly CTA_REGEX;
46
+ /**
47
+ * Analyzes an article's engagement mechanics.
48
+ */
49
+ static analyze(content: string): ArticleEngagementReport;
50
+ }
package/dist/index.d.ts CHANGED
@@ -122,6 +122,9 @@ import { ContextTemplatesGenerator } from "./context-templates-generator";
122
122
  import { SearchIntentClassifier } from "./search-intent-classifier";
123
123
  import { SeoOpportunityPrioritizer } from "./seo-opportunity-prioritizer";
124
124
  import { TrustSignalsExtractor } from "./trust-signals-extractor";
125
+ import { CompetitorGapBlueprintEngine } from "./competitor-gap-blueprint";
126
+ import { EngagementHookAnalyzer } from "./engagement-hook-analyzer";
127
+ import { KeywordDensityGuard } from "./keyword-density-guard";
125
128
  export declare function createLynxSeoEngine(config: EngineConfig): LynxSeoEngine;
126
129
  export declare const LynxSeo: {
127
130
  createEngine: typeof createLynxSeoEngine;
@@ -141,6 +144,9 @@ export declare const LynxSeo: {
141
144
  searchIntent: typeof SearchIntentClassifier;
142
145
  opportunityPrioritizer: typeof SeoOpportunityPrioritizer;
143
146
  trustSignals: typeof TrustSignalsExtractor;
147
+ competitorGap: typeof CompetitorGapBlueprintEngine;
148
+ engagementHook: typeof EngagementHookAnalyzer;
149
+ keywordDensity: typeof KeywordDensityGuard;
144
150
  inspectMeta: typeof SiteAuditor.inspectMeta;
145
151
  crawlDomain: typeof DeepCrawlerAuditor.crawlAndAuditDomain;
146
152
  inspectHtmlSnapshot: typeof DeepCrawlerAuditor.inspectHtmlSnapshot;
@@ -182,6 +188,9 @@ export declare const LynxSeo: {
182
188
  marketingSkills: typeof MarketingSkillsEngine;
183
189
  regenerationTracker: typeof PageRegenerationTracker;
184
190
  };
191
+ export * from "./competitor-gap-blueprint";
192
+ export * from "./engagement-hook-analyzer";
193
+ export * from "./keyword-density-guard";
185
194
  export * from "./search-intent-classifier";
186
195
  export * from "./seo-opportunity-prioritizer";
187
196
  export * from "./trust-signals-extractor";
package/dist/index.js CHANGED
@@ -10782,6 +10782,273 @@ class TrustSignalsExtractor {
10782
10782
  }
10783
10783
  }
10784
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
+
10785
11052
  // src/index.ts
10786
11053
  function createLynxSeoEngine(config) {
10787
11054
  return new LynxSeoEngine(config);
@@ -10804,6 +11071,9 @@ var LynxSeo = {
10804
11071
  searchIntent: SearchIntentClassifier,
10805
11072
  opportunityPrioritizer: SeoOpportunityPrioritizer,
10806
11073
  trustSignals: TrustSignalsExtractor,
11074
+ competitorGap: CompetitorGapBlueprintEngine,
11075
+ engagementHook: EngagementHookAnalyzer,
11076
+ keywordDensity: KeywordDensityGuard,
10807
11077
  inspectMeta: SiteAuditor.inspectMeta,
10808
11078
  crawlDomain: DeepCrawlerAuditor.crawlAndAuditDomain,
10809
11079
  inspectHtmlSnapshot: DeepCrawlerAuditor.inspectHtmlSnapshot,
@@ -10911,6 +11181,7 @@ export {
10911
11181
  KnowledgeGraphLinker,
10912
11182
  KnowledgeBankBuilder,
10913
11183
  KeywordPermutatorEngine,
11184
+ KeywordDensityGuard,
10914
11185
  IsrCacheManager,
10915
11186
  InternalPageRankEngine,
10916
11187
  InstantMatrixSearchEngine,
@@ -10926,12 +11197,14 @@ export {
10926
11197
  FeaturesKnowledgeHarvester,
10927
11198
  ExtendedSchemaGraphBuilder,
10928
11199
  ExistingMediaHarvester,
11200
+ EngagementHookAnalyzer,
10929
11201
  EmbeddableSeoWidgetGenerator,
10930
11202
  DeepCrawlerAuditor,
10931
11203
  CrosslinkScorerEngine,
10932
11204
  CroCopywritingEngine,
10933
11205
  CopywritingFrameworksMaster,
10934
11206
  ContextTemplatesGenerator,
11207
+ CompetitorGapBlueprintEngine,
10935
11208
  CONTENT_AI_40_TOOLS,
10936
11209
  BrandDnaCalendarEngine,
10937
11210
  BacklinksClient,
package/dist/index.mjs CHANGED
@@ -10782,6 +10782,273 @@ class TrustSignalsExtractor {
10782
10782
  }
10783
10783
  }
10784
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
+
10785
11052
  // src/index.ts
10786
11053
  function createLynxSeoEngine(config) {
10787
11054
  return new LynxSeoEngine(config);
@@ -10804,6 +11071,9 @@ var LynxSeo = {
10804
11071
  searchIntent: SearchIntentClassifier,
10805
11072
  opportunityPrioritizer: SeoOpportunityPrioritizer,
10806
11073
  trustSignals: TrustSignalsExtractor,
11074
+ competitorGap: CompetitorGapBlueprintEngine,
11075
+ engagementHook: EngagementHookAnalyzer,
11076
+ keywordDensity: KeywordDensityGuard,
10807
11077
  inspectMeta: SiteAuditor.inspectMeta,
10808
11078
  crawlDomain: DeepCrawlerAuditor.crawlAndAuditDomain,
10809
11079
  inspectHtmlSnapshot: DeepCrawlerAuditor.inspectHtmlSnapshot,
@@ -10911,6 +11181,7 @@ export {
10911
11181
  KnowledgeGraphLinker,
10912
11182
  KnowledgeBankBuilder,
10913
11183
  KeywordPermutatorEngine,
11184
+ KeywordDensityGuard,
10914
11185
  IsrCacheManager,
10915
11186
  InternalPageRankEngine,
10916
11187
  InstantMatrixSearchEngine,
@@ -10926,12 +11197,14 @@ export {
10926
11197
  FeaturesKnowledgeHarvester,
10927
11198
  ExtendedSchemaGraphBuilder,
10928
11199
  ExistingMediaHarvester,
11200
+ EngagementHookAnalyzer,
10929
11201
  EmbeddableSeoWidgetGenerator,
10930
11202
  DeepCrawlerAuditor,
10931
11203
  CrosslinkScorerEngine,
10932
11204
  CroCopywritingEngine,
10933
11205
  CopywritingFrameworksMaster,
10934
11206
  ContextTemplatesGenerator,
11207
+ CompetitorGapBlueprintEngine,
10935
11208
  CONTENT_AI_40_TOOLS,
10936
11209
  BrandDnaCalendarEngine,
10937
11210
  BacklinksClient,
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 🛡️ Keyword Density Guard & Section Distribution Heatmap
3
+ * Ported & optimized from Python keyword_analyzer.py.
4
+ * Calculates exact keyword density, detects keyword stuffing risks (> 2.2%),
5
+ * and validates placement across Title, H1, First 100 Words, H2s, and FAQ.
6
+ */
7
+ export interface KeywordPlacementCheck {
8
+ location: "title" | "h1" | "first_100_words" | "h2_headings" | "conclusion" | "faq";
9
+ present: boolean;
10
+ count: number;
11
+ }
12
+ export interface KeywordAnalysisReport {
13
+ keyword: string;
14
+ totalOccurrences: number;
15
+ totalWords: number;
16
+ densityPercent: number;
17
+ status: "too_low" | "optimal" | "stuffing_risk";
18
+ placements: KeywordPlacementCheck[];
19
+ consecutiveSentencesWithKeyword: number;
20
+ stuffingAlert: boolean;
21
+ recommendations: string[];
22
+ }
23
+ export declare class KeywordDensityGuard {
24
+ /**
25
+ * Analyzes keyword density and placement integrity.
26
+ */
27
+ static audit(content: string, primaryKeyword: string, metaTitle?: string): KeywordAnalysisReport;
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxflow/seo-engine",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "High-Performance Universal Programmatic SEO & AI Search Engine SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",