@lynxflow/seo-engine 2.4.0 → 2.6.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/content-quality-scorer.d.ts +40 -0
- package/dist/engagement-hook-analyzer.d.ts +50 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +596 -0
- package/dist/index.mjs +596 -0
- package/dist/keyword-density-guard.d.ts +28 -0
- package/dist/landing-cro-checker.d.ts +36 -0
- package/dist/social-community-insights.d.ts +27 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10782,6 +10782,590 @@ 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
|
+
|
|
11052
|
+
// src/content-quality-scorer.ts
|
|
11053
|
+
class ContentQualityScorer {
|
|
11054
|
+
static VAGUE_WORDS = [
|
|
11055
|
+
/\b(?:many|some|various|numerous|several|often|sometimes|usually|generally|typically|significantly|substantial|greatly|very|really|quite|relatively)\b/gi
|
|
11056
|
+
];
|
|
11057
|
+
static SPECIFICITY_DATA_PATTERNS = [
|
|
11058
|
+
/\b\d{1,3}%\b/g,
|
|
11059
|
+
/(?:\$|€|£)\s*[\d,]+(?:\.\d{2})?\b/g,
|
|
11060
|
+
/\b\d{4}\b/g,
|
|
11061
|
+
/\b\d+(?:,\d{3})*\s*(?:users?|customers?|businesses?|downloads?|hours?|minutes?)\b/gi,
|
|
11062
|
+
/\"[^\"]{10,120}\"/g
|
|
11063
|
+
];
|
|
11064
|
+
static CONVERSATIONAL_DEVICES = [
|
|
11065
|
+
/\([^)]{5,40}\)/g,
|
|
11066
|
+
/\?(?:\s|$)/g,
|
|
11067
|
+
/\b(?:don't|can't|won't|you're|you've|it's|that's|here's|let's|we've|we're|I've)\b/gi,
|
|
11068
|
+
/(?:^|\.\s+)(?:Look|Here['’]s the thing|The truth is|Sound familiar|Trust me|Regardez|En clair)/gi
|
|
11069
|
+
];
|
|
11070
|
+
static scoreContent(content, metadata) {
|
|
11071
|
+
if (!content || content.trim().length === 0) {
|
|
11072
|
+
return this.emptyReport();
|
|
11073
|
+
}
|
|
11074
|
+
const words = content.split(/\s+/).filter(Boolean);
|
|
11075
|
+
const totalWords = words.length;
|
|
11076
|
+
let conversationalHits = 0;
|
|
11077
|
+
for (const pat of this.CONVERSATIONAL_DEVICES) {
|
|
11078
|
+
conversationalHits += (content.match(pat) || []).length;
|
|
11079
|
+
}
|
|
11080
|
+
const humanityRaw = Math.min(100, Math.round(conversationalHits / Math.max(1, totalWords / 80) * 60) + 30);
|
|
11081
|
+
const humanityScore = Math.max(20, Math.min(100, humanityRaw));
|
|
11082
|
+
let dataPointsCount = 0;
|
|
11083
|
+
for (const pat of this.SPECIFICITY_DATA_PATTERNS) {
|
|
11084
|
+
dataPointsCount += (content.match(pat) || []).length;
|
|
11085
|
+
}
|
|
11086
|
+
let vagueWordsCount = 0;
|
|
11087
|
+
for (const pat of this.VAGUE_WORDS) {
|
|
11088
|
+
vagueWordsCount += (content.match(pat) || []).length;
|
|
11089
|
+
}
|
|
11090
|
+
const specificityRatio = dataPointsCount / Math.max(1, vagueWordsCount * 0.5 + 2);
|
|
11091
|
+
const specificityScore = Math.max(25, Math.min(100, Math.round(specificityRatio * 35 + 40)));
|
|
11092
|
+
const lines = content.split(`
|
|
11093
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
11094
|
+
const listLines = lines.filter((l) => l.startsWith("-") || l.startsWith("*") || l.startsWith("1.") || l.startsWith("2.")).length;
|
|
11095
|
+
const proseLines = lines.filter((l) => !l.startsWith("#") && !l.startsWith("-") && !l.startsWith("*") && !l.startsWith("1.")).length;
|
|
11096
|
+
const totalContentLines = Math.max(1, listLines + proseLines);
|
|
11097
|
+
const proseRatio = Math.round(proseLines / totalContentLines * 100) / 100;
|
|
11098
|
+
let structureScore = 85;
|
|
11099
|
+
const structureIssues = [];
|
|
11100
|
+
if (proseRatio < 0.4) {
|
|
11101
|
+
structureScore = 50;
|
|
11102
|
+
structureIssues.push("Too list-heavy (> 60% bullet points). Add explanatory narrative prose paragraphs between lists.");
|
|
11103
|
+
} else if (proseRatio > 0.85) {
|
|
11104
|
+
structureScore = 65;
|
|
11105
|
+
structureIssues.push("Too prose-heavy (> 85% text walls). Break up long sections with bullet lists, comparison tables, or callouts.");
|
|
11106
|
+
}
|
|
11107
|
+
let seoScore = 80;
|
|
11108
|
+
const seoIssues = [];
|
|
11109
|
+
const hasH1 = lines.some((l) => l.startsWith("# "));
|
|
11110
|
+
const h2Count = lines.filter((l) => l.startsWith("## ")).length;
|
|
11111
|
+
if (!hasH1) {
|
|
11112
|
+
seoScore -= 25;
|
|
11113
|
+
seoIssues.push("Missing H1 title heading.");
|
|
11114
|
+
}
|
|
11115
|
+
if (h2Count < 2) {
|
|
11116
|
+
seoScore -= 20;
|
|
11117
|
+
seoIssues.push("Fewer than 2 H2 subheadings found. Structure content with clear sub-topics.");
|
|
11118
|
+
}
|
|
11119
|
+
if (metadata?.primaryKeyword && !content.toLowerCase().includes(metadata.primaryKeyword.toLowerCase())) {
|
|
11120
|
+
seoScore -= 20;
|
|
11121
|
+
seoIssues.push(`Target keyword "${metadata.primaryKeyword}" is missing from body text.`);
|
|
11122
|
+
}
|
|
11123
|
+
const avgSentenceLength = Math.round(totalWords / Math.max(1, content.split(/[.?!]/).length) * 10) / 10;
|
|
11124
|
+
let readabilityScore = 85;
|
|
11125
|
+
const readIssues = [];
|
|
11126
|
+
if (avgSentenceLength > 20) {
|
|
11127
|
+
readabilityScore = 55;
|
|
11128
|
+
readIssues.push(`Average sentence length is ${avgSentenceLength} words (Target: 13-17 words).`);
|
|
11129
|
+
}
|
|
11130
|
+
const composite = Math.round(humanityScore * 0.3 + specificityScore * 0.25 + structureScore * 0.2 + seoScore * 0.15 + readabilityScore * 0.1);
|
|
11131
|
+
const passes = composite >= 70;
|
|
11132
|
+
let grade = "F";
|
|
11133
|
+
if (composite >= 90)
|
|
11134
|
+
grade = "A+";
|
|
11135
|
+
else if (composite >= 80)
|
|
11136
|
+
grade = "A";
|
|
11137
|
+
else if (composite >= 70)
|
|
11138
|
+
grade = "B";
|
|
11139
|
+
else if (composite >= 55)
|
|
11140
|
+
grade = "C";
|
|
11141
|
+
const topFixes = [
|
|
11142
|
+
...structureIssues,
|
|
11143
|
+
...seoIssues,
|
|
11144
|
+
...readIssues
|
|
11145
|
+
];
|
|
11146
|
+
if (specificityScore < 70) {
|
|
11147
|
+
topFixes.push("Inject verified data points (specific dollar amounts, percentage improvements, dates) instead of vague generalities.");
|
|
11148
|
+
}
|
|
11149
|
+
if (humanityScore < 70) {
|
|
11150
|
+
topFixes.push("Adopt a conversational tone: add contractions (you're, it's), rhetorical questions, and casual transition words.");
|
|
11151
|
+
}
|
|
11152
|
+
return {
|
|
11153
|
+
compositeScore: composite,
|
|
11154
|
+
passesQualityThreshold: passes,
|
|
11155
|
+
grade,
|
|
11156
|
+
dimensions: {
|
|
11157
|
+
humanity: {
|
|
11158
|
+
score: humanityScore,
|
|
11159
|
+
weight: 0.3,
|
|
11160
|
+
weightedScore: Math.round(humanityScore * 0.3),
|
|
11161
|
+
strengths: humanityScore >= 75 ? ["Conversational tone", "Natural pacing"] : [],
|
|
11162
|
+
issues: humanityScore < 75 ? ["Sounds slightly robotic or formal"] : []
|
|
11163
|
+
},
|
|
11164
|
+
specificity: {
|
|
11165
|
+
score: specificityScore,
|
|
11166
|
+
weight: 0.25,
|
|
11167
|
+
weightedScore: Math.round(specificityScore * 0.25),
|
|
11168
|
+
strengths: specificityScore >= 75 ? ["High hard-data density", "Concrete benchmarks"] : [],
|
|
11169
|
+
issues: specificityScore < 75 ? ["Excessive vague words (many, several, often)"] : []
|
|
11170
|
+
},
|
|
11171
|
+
structureBalance: {
|
|
11172
|
+
score: structureScore,
|
|
11173
|
+
weight: 0.2,
|
|
11174
|
+
weightedScore: Math.round(structureScore * 0.2),
|
|
11175
|
+
strengths: structureScore >= 75 ? ["Balanced mix of prose and scannable lists"] : [],
|
|
11176
|
+
issues: structureIssues
|
|
11177
|
+
},
|
|
11178
|
+
seoCompliance: {
|
|
11179
|
+
score: seoScore,
|
|
11180
|
+
weight: 0.15,
|
|
11181
|
+
weightedScore: Math.round(seoScore * 0.15),
|
|
11182
|
+
strengths: seoScore >= 75 ? ["Proper H1/H2 hierarchy"] : [],
|
|
11183
|
+
issues: seoIssues
|
|
11184
|
+
},
|
|
11185
|
+
readability: {
|
|
11186
|
+
score: readabilityScore,
|
|
11187
|
+
weight: 0.1,
|
|
11188
|
+
weightedScore: Math.round(readabilityScore * 0.1),
|
|
11189
|
+
strengths: readabilityScore >= 75 ? ["Punchy sentence lengths"] : [],
|
|
11190
|
+
issues: readIssues
|
|
11191
|
+
}
|
|
11192
|
+
},
|
|
11193
|
+
proseToListRatio: proseRatio,
|
|
11194
|
+
topPriorityFixes: topFixes
|
|
11195
|
+
};
|
|
11196
|
+
}
|
|
11197
|
+
static emptyReport() {
|
|
11198
|
+
return {
|
|
11199
|
+
compositeScore: 0,
|
|
11200
|
+
passesQualityThreshold: false,
|
|
11201
|
+
grade: "F",
|
|
11202
|
+
dimensions: {
|
|
11203
|
+
humanity: { score: 0, weight: 0.3, weightedScore: 0, strengths: [], issues: [] },
|
|
11204
|
+
specificity: { score: 0, weight: 0.25, weightedScore: 0, strengths: [], issues: [] },
|
|
11205
|
+
structureBalance: { score: 0, weight: 0.2, weightedScore: 0, strengths: [], issues: [] },
|
|
11206
|
+
seoCompliance: { score: 0, weight: 0.15, weightedScore: 0, strengths: [], issues: [] },
|
|
11207
|
+
readability: { score: 0, weight: 0.1, weightedScore: 0, strengths: [], issues: [] }
|
|
11208
|
+
},
|
|
11209
|
+
proseToListRatio: 0,
|
|
11210
|
+
topPriorityFixes: ["Provide text content to score."]
|
|
11211
|
+
};
|
|
11212
|
+
}
|
|
11213
|
+
}
|
|
11214
|
+
|
|
11215
|
+
// src/landing-cro-checker.ts
|
|
11216
|
+
class LandingCroChecker {
|
|
11217
|
+
static auditLandingPage(content, config = {}) {
|
|
11218
|
+
const checks = [];
|
|
11219
|
+
const lines = content.split(`
|
|
11220
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
11221
|
+
const h1 = lines.find((l) => l.startsWith("# "))?.replace("# ", "") || "";
|
|
11222
|
+
const h1Words = h1.split(/\s+/).filter(Boolean).length;
|
|
11223
|
+
const isH1Clear = h1Words >= 4 && h1Words <= 14;
|
|
11224
|
+
checks.push({
|
|
11225
|
+
id: "h1_length",
|
|
11226
|
+
name: "Headline Length & Outcome Focus",
|
|
11227
|
+
category: "headline",
|
|
11228
|
+
passed: isH1Clear,
|
|
11229
|
+
importance: "critical",
|
|
11230
|
+
feedback: isH1Clear ? "H1 is clear, outcome-focused, and scannable in < 3s." : "H1 is missing or too long/vague."
|
|
11231
|
+
});
|
|
11232
|
+
const hasValueProp = /save|boost|cut|grow|automate|increase|réduire|augmenter|gagner/i.test(content);
|
|
11233
|
+
checks.push({
|
|
11234
|
+
id: "value_prop_benefit",
|
|
11235
|
+
name: "Concrete Benefit & Outcome",
|
|
11236
|
+
category: "value_prop",
|
|
11237
|
+
passed: hasValueProp,
|
|
11238
|
+
importance: "critical",
|
|
11239
|
+
feedback: hasValueProp ? "Explicit outcome/benefit articulated in content." : "Lacks explicit outcome/benefit statements."
|
|
11240
|
+
});
|
|
11241
|
+
const hasReviews = /rated|reviews?|avis|clients?|customers?|businesses?|\d{1,3}(?:,\d{3})*\+/i.test(content);
|
|
11242
|
+
checks.push({
|
|
11243
|
+
id: "social_proof_ratings",
|
|
11244
|
+
name: "Customer Reviews & Numbers",
|
|
11245
|
+
category: "social_proof",
|
|
11246
|
+
passed: hasReviews,
|
|
11247
|
+
importance: "important",
|
|
11248
|
+
feedback: hasReviews ? "Social proof count or star rating present." : "No social proof numbers found."
|
|
11249
|
+
});
|
|
11250
|
+
const hasActionCta = /(?:start|try|get|claim|démarrer|essayer|profiter|obtenir)\s+(?:free|trial|now|today|gratuit)/i.test(content);
|
|
11251
|
+
checks.push({
|
|
11252
|
+
id: "cta_action_verb",
|
|
11253
|
+
name: "High-Contrast Outcome CTA",
|
|
11254
|
+
category: "ctas",
|
|
11255
|
+
passed: hasActionCta,
|
|
11256
|
+
importance: "critical",
|
|
11257
|
+
feedback: hasActionCta ? "High-converting action verb present in CTA." : "CTA is generic or missing action verb."
|
|
11258
|
+
});
|
|
11259
|
+
const hasFaq = /faq|frequently asked|questions|foire aux questions/i.test(content);
|
|
11260
|
+
checks.push({
|
|
11261
|
+
id: "faq_objection_handling",
|
|
11262
|
+
name: "Objection Handling & FAQ Section",
|
|
11263
|
+
category: "objection_handling",
|
|
11264
|
+
passed: hasFaq,
|
|
11265
|
+
importance: "important",
|
|
11266
|
+
feedback: hasFaq ? "FAQ section handles user hesitations." : "Missing FAQ / objection handling section."
|
|
11267
|
+
});
|
|
11268
|
+
const hasRiskReversal = /no credit card|cancel anytime|money-back|sans carte|sans engagement|garantie/i.test(content);
|
|
11269
|
+
checks.push({
|
|
11270
|
+
id: "risk_reversal_guarantee",
|
|
11271
|
+
name: "Zero-Risk Micro-Copy",
|
|
11272
|
+
category: "risk_reversal",
|
|
11273
|
+
passed: hasRiskReversal,
|
|
11274
|
+
importance: "important",
|
|
11275
|
+
feedback: hasRiskReversal ? "Risk reversal micro-copy found near CTA." : "No risk reversal guarantee found."
|
|
11276
|
+
});
|
|
11277
|
+
const hasUrgency = /today|now|instant|limited|aujourd'hui|maintenant|immédiat/i.test(content);
|
|
11278
|
+
checks.push({
|
|
11279
|
+
id: "urgency_trigger",
|
|
11280
|
+
name: "Immediate Access & Momentum",
|
|
11281
|
+
category: "urgency",
|
|
11282
|
+
passed: hasUrgency,
|
|
11283
|
+
importance: "enhancement",
|
|
11284
|
+
feedback: hasUrgency ? "Presents immediate access momentum." : "Lacks urgency or immediacy trigger."
|
|
11285
|
+
});
|
|
11286
|
+
const hasSubheadings = lines.filter((l) => l.startsWith("## ")).length >= 3;
|
|
11287
|
+
checks.push({
|
|
11288
|
+
id: "structure_scannability",
|
|
11289
|
+
name: "Visual Hierarchy & H2 Subheadings",
|
|
11290
|
+
category: "structure",
|
|
11291
|
+
passed: hasSubheadings,
|
|
11292
|
+
importance: "important",
|
|
11293
|
+
feedback: hasSubheadings ? "Clean visual hierarchy with 3+ H2 sections." : "Fewer than 3 H2 subheadings found."
|
|
11294
|
+
});
|
|
11295
|
+
const totalChecks = checks.length;
|
|
11296
|
+
const passedChecks = checks.filter((c) => c.passed).length;
|
|
11297
|
+
const criticalFailures = checks.filter((c) => !c.passed && c.importance === "critical");
|
|
11298
|
+
const warnings = checks.filter((c) => !c.passed && c.importance === "important");
|
|
11299
|
+
const overallCroScore = Math.round(passedChecks / totalChecks * 100);
|
|
11300
|
+
const passes = overallCroScore >= 70 && criticalFailures.length === 0;
|
|
11301
|
+
let grade = "F";
|
|
11302
|
+
if (overallCroScore >= 90)
|
|
11303
|
+
grade = "A+";
|
|
11304
|
+
else if (overallCroScore >= 80)
|
|
11305
|
+
grade = "A";
|
|
11306
|
+
else if (overallCroScore >= 70)
|
|
11307
|
+
grade = "B";
|
|
11308
|
+
else if (overallCroScore >= 55)
|
|
11309
|
+
grade = "C";
|
|
11310
|
+
const categoryScores = {};
|
|
11311
|
+
for (const check of checks) {
|
|
11312
|
+
if (!categoryScores[check.category]) {
|
|
11313
|
+
categoryScores[check.category] = { score: 0, passedCount: 0, totalCount: 0 };
|
|
11314
|
+
}
|
|
11315
|
+
categoryScores[check.category].totalCount++;
|
|
11316
|
+
if (check.passed)
|
|
11317
|
+
categoryScores[check.category].passedCount++;
|
|
11318
|
+
}
|
|
11319
|
+
for (const cat in categoryScores) {
|
|
11320
|
+
categoryScores[cat].score = Math.round(categoryScores[cat].passedCount / categoryScores[cat].totalCount * 100);
|
|
11321
|
+
}
|
|
11322
|
+
const roadmap = checks.filter((c) => !c.passed).map((c) => `[${c.importance.toUpperCase()}] ${c.category}: ${c.feedback}`);
|
|
11323
|
+
return {
|
|
11324
|
+
overallCroScore,
|
|
11325
|
+
passesCroAudit: passes,
|
|
11326
|
+
grade,
|
|
11327
|
+
criticalFailuresCount: criticalFailures.length,
|
|
11328
|
+
warningsCount: warnings.length,
|
|
11329
|
+
checks,
|
|
11330
|
+
categoryScores,
|
|
11331
|
+
conversionRoadmap: roadmap
|
|
11332
|
+
};
|
|
11333
|
+
}
|
|
11334
|
+
}
|
|
11335
|
+
|
|
11336
|
+
// src/social-community-insights.ts
|
|
11337
|
+
class SocialCommunityInsightsAggregator {
|
|
11338
|
+
static aggregate(topic, rawInsights) {
|
|
11339
|
+
const painPoints = [];
|
|
11340
|
+
const realLanguage = [];
|
|
11341
|
+
const quotables = [];
|
|
11342
|
+
for (const item of rawInsights) {
|
|
11343
|
+
if (item.type === "pain_point" || item.type === "complaint") {
|
|
11344
|
+
if (!painPoints.includes(item.quotableText)) {
|
|
11345
|
+
painPoints.push(item.quotableText);
|
|
11346
|
+
}
|
|
11347
|
+
}
|
|
11348
|
+
if (item.quotableText) {
|
|
11349
|
+
quotables.push(`"${item.quotableText}" (${item.source})`);
|
|
11350
|
+
}
|
|
11351
|
+
for (const phrase of item.userVocabulary) {
|
|
11352
|
+
if (!realLanguage.includes(phrase)) {
|
|
11353
|
+
realLanguage.push(phrase);
|
|
11354
|
+
}
|
|
11355
|
+
}
|
|
11356
|
+
}
|
|
11357
|
+
const recommendedAngle = painPoints.length > 0 ? `Address the #1 community frustration: "${painPoints[0]}" by demonstrating immediate resolution in the hero hook.` : `Highlight community-tested best practices and back every recommendation with verified user metrics.`;
|
|
11358
|
+
return {
|
|
11359
|
+
topic,
|
|
11360
|
+
totalInsights: rawInsights.length,
|
|
11361
|
+
painPoints,
|
|
11362
|
+
realLanguagePhrases: realLanguage,
|
|
11363
|
+
quotableExcerpts: quotables,
|
|
11364
|
+
recommendedAngleForCopy: recommendedAngle
|
|
11365
|
+
};
|
|
11366
|
+
}
|
|
11367
|
+
}
|
|
11368
|
+
|
|
10785
11369
|
// src/index.ts
|
|
10786
11370
|
function createLynxSeoEngine(config) {
|
|
10787
11371
|
return new LynxSeoEngine(config);
|
|
@@ -10804,6 +11388,12 @@ var LynxSeo = {
|
|
|
10804
11388
|
searchIntent: SearchIntentClassifier,
|
|
10805
11389
|
opportunityPrioritizer: SeoOpportunityPrioritizer,
|
|
10806
11390
|
trustSignals: TrustSignalsExtractor,
|
|
11391
|
+
competitorGap: CompetitorGapBlueprintEngine,
|
|
11392
|
+
engagementHook: EngagementHookAnalyzer,
|
|
11393
|
+
keywordDensity: KeywordDensityGuard,
|
|
11394
|
+
contentQuality: ContentQualityScorer,
|
|
11395
|
+
landingCroChecker: LandingCroChecker,
|
|
11396
|
+
socialInsights: SocialCommunityInsightsAggregator,
|
|
10807
11397
|
inspectMeta: SiteAuditor.inspectMeta,
|
|
10808
11398
|
crawlDomain: DeepCrawlerAuditor.crawlAndAuditDomain,
|
|
10809
11399
|
inspectHtmlSnapshot: DeepCrawlerAuditor.inspectHtmlSnapshot,
|
|
@@ -10870,6 +11460,7 @@ export {
|
|
|
10870
11460
|
SocialVideoSeoAnalyticsEngine,
|
|
10871
11461
|
SocialTrendSeoGenerator,
|
|
10872
11462
|
SocialGrowthSuite,
|
|
11463
|
+
SocialCommunityInsightsAggregator,
|
|
10873
11464
|
SocialAdsSocialSeoEngine,
|
|
10874
11465
|
SiteAuditor,
|
|
10875
11466
|
SerpRankHistoryEngine,
|
|
@@ -10907,10 +11498,12 @@ export {
|
|
|
10907
11498
|
LynxAnalyticsClient,
|
|
10908
11499
|
LlmContentCleaner,
|
|
10909
11500
|
LegalDisclaimerEngine,
|
|
11501
|
+
LandingCroChecker,
|
|
10910
11502
|
LagoTokenMeter,
|
|
10911
11503
|
KnowledgeGraphLinker,
|
|
10912
11504
|
KnowledgeBankBuilder,
|
|
10913
11505
|
KeywordPermutatorEngine,
|
|
11506
|
+
KeywordDensityGuard,
|
|
10914
11507
|
IsrCacheManager,
|
|
10915
11508
|
InternalPageRankEngine,
|
|
10916
11509
|
InstantMatrixSearchEngine,
|
|
@@ -10926,12 +11519,15 @@ export {
|
|
|
10926
11519
|
FeaturesKnowledgeHarvester,
|
|
10927
11520
|
ExtendedSchemaGraphBuilder,
|
|
10928
11521
|
ExistingMediaHarvester,
|
|
11522
|
+
EngagementHookAnalyzer,
|
|
10929
11523
|
EmbeddableSeoWidgetGenerator,
|
|
10930
11524
|
DeepCrawlerAuditor,
|
|
10931
11525
|
CrosslinkScorerEngine,
|
|
10932
11526
|
CroCopywritingEngine,
|
|
10933
11527
|
CopywritingFrameworksMaster,
|
|
10934
11528
|
ContextTemplatesGenerator,
|
|
11529
|
+
ContentQualityScorer,
|
|
11530
|
+
CompetitorGapBlueprintEngine,
|
|
10935
11531
|
CONTENT_AI_40_TOOLS,
|
|
10936
11532
|
BrandDnaCalendarEngine,
|
|
10937
11533
|
BacklinksClient,
|