@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 +1 -1
- package/dist/competitor-gap-blueprint.d.ts +34 -0
- package/dist/engagement-hook-analyzer.d.ts +50 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +273 -0
- package/dist/index.mjs +273 -0
- package/dist/keyword-density-guard.d.ts +28 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ High-Performance Universal Programmatic SEO & Structured Data Engine for Modern
|
|
|
4
4
|
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
[](LICENSE)
|
|
7
|
-
[](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
|
+
}
|