@rankcli/agent-runtime 0.0.1
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 +242 -0
- package/dist/analyzer-2CSWIQGD.mjs +6 -0
- package/dist/chunk-YNZYHEYM.mjs +774 -0
- package/dist/index.d.mts +4012 -0
- package/dist/index.d.ts +4012 -0
- package/dist/index.js +29672 -0
- package/dist/index.mjs +28602 -0
- package/package.json +53 -0
- package/scripts/build-deno.ts +134 -0
- package/src/audit/ai/analyzer.ts +347 -0
- package/src/audit/ai/index.ts +29 -0
- package/src/audit/ai/prompts/content-analysis.ts +271 -0
- package/src/audit/ai/types.ts +179 -0
- package/src/audit/checks/additional-checks.ts +439 -0
- package/src/audit/checks/ai-citation-worthiness.ts +399 -0
- package/src/audit/checks/ai-content-structure.ts +325 -0
- package/src/audit/checks/ai-readiness.ts +339 -0
- package/src/audit/checks/anchor-text.ts +179 -0
- package/src/audit/checks/answer-conciseness.ts +322 -0
- package/src/audit/checks/asset-minification.ts +270 -0
- package/src/audit/checks/bing-optimization.ts +206 -0
- package/src/audit/checks/brand-mention-optimization.ts +349 -0
- package/src/audit/checks/caching-headers.ts +305 -0
- package/src/audit/checks/canonical-advanced.ts +150 -0
- package/src/audit/checks/canonical-domain.ts +196 -0
- package/src/audit/checks/citation-quality.ts +358 -0
- package/src/audit/checks/client-rendering.ts +542 -0
- package/src/audit/checks/color-contrast.ts +342 -0
- package/src/audit/checks/content-freshness.ts +170 -0
- package/src/audit/checks/content-science.ts +589 -0
- package/src/audit/checks/conversion-elements.ts +526 -0
- package/src/audit/checks/crawlability.ts +220 -0
- package/src/audit/checks/directory-listing.ts +172 -0
- package/src/audit/checks/dom-analysis.ts +191 -0
- package/src/audit/checks/dom-size.ts +246 -0
- package/src/audit/checks/duplicate-content.ts +194 -0
- package/src/audit/checks/eeat-signals.ts +990 -0
- package/src/audit/checks/entity-seo.ts +396 -0
- package/src/audit/checks/featured-snippet.ts +473 -0
- package/src/audit/checks/freshness-signals.ts +443 -0
- package/src/audit/checks/funnel-intent.ts +463 -0
- package/src/audit/checks/hreflang.ts +174 -0
- package/src/audit/checks/html-compliance.ts +302 -0
- package/src/audit/checks/image-dimensions.ts +167 -0
- package/src/audit/checks/images.ts +160 -0
- package/src/audit/checks/indexnow.ts +275 -0
- package/src/audit/checks/interactive-tools.ts +475 -0
- package/src/audit/checks/internal-link-graph.ts +436 -0
- package/src/audit/checks/keyword-analysis.ts +239 -0
- package/src/audit/checks/keyword-cannibalization.ts +385 -0
- package/src/audit/checks/keyword-placement.ts +471 -0
- package/src/audit/checks/links.ts +203 -0
- package/src/audit/checks/llms-txt.ts +224 -0
- package/src/audit/checks/local-seo.ts +296 -0
- package/src/audit/checks/mobile.ts +167 -0
- package/src/audit/checks/modern-images.ts +226 -0
- package/src/audit/checks/navboost-signals.ts +395 -0
- package/src/audit/checks/on-page.ts +209 -0
- package/src/audit/checks/page-resources.ts +285 -0
- package/src/audit/checks/pagination.ts +180 -0
- package/src/audit/checks/performance.ts +153 -0
- package/src/audit/checks/platform-presence.ts +580 -0
- package/src/audit/checks/redirect-analysis.ts +153 -0
- package/src/audit/checks/redirect-chain.ts +389 -0
- package/src/audit/checks/resource-hints.ts +420 -0
- package/src/audit/checks/responsive-css.ts +247 -0
- package/src/audit/checks/responsive-images.ts +396 -0
- package/src/audit/checks/review-ecosystem.ts +415 -0
- package/src/audit/checks/robots-validation.ts +373 -0
- package/src/audit/checks/security-headers.ts +172 -0
- package/src/audit/checks/security.ts +144 -0
- package/src/audit/checks/serp-preview.ts +251 -0
- package/src/audit/checks/site-maturity.ts +444 -0
- package/src/audit/checks/social-meta.test.ts +275 -0
- package/src/audit/checks/social-meta.ts +134 -0
- package/src/audit/checks/soft-404.ts +151 -0
- package/src/audit/checks/structured-data.ts +238 -0
- package/src/audit/checks/tech-detection.ts +496 -0
- package/src/audit/checks/topical-clusters.ts +435 -0
- package/src/audit/checks/tracker-bloat.ts +462 -0
- package/src/audit/checks/tracking-verification.test.ts +371 -0
- package/src/audit/checks/tracking-verification.ts +636 -0
- package/src/audit/checks/url-safety.ts +682 -0
- package/src/audit/deno-entry.ts +66 -0
- package/src/audit/discovery/index.ts +15 -0
- package/src/audit/discovery/link-crawler.ts +232 -0
- package/src/audit/discovery/repo-routes.ts +347 -0
- package/src/audit/engine.ts +620 -0
- package/src/audit/fixes/index.ts +209 -0
- package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
- package/src/audit/fixes/social-meta-fixes.ts +463 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/runner.test.ts +299 -0
- package/src/audit/runner.ts +130 -0
- package/src/audit/types.ts +1953 -0
- package/src/content/featured-snippet.ts +367 -0
- package/src/content/generator.test.ts +534 -0
- package/src/content/generator.ts +501 -0
- package/src/content/headline.ts +317 -0
- package/src/content/index.ts +62 -0
- package/src/content/intent.ts +258 -0
- package/src/content/keyword-density.ts +349 -0
- package/src/content/readability.ts +262 -0
- package/src/executor.ts +336 -0
- package/src/fixer.ts +416 -0
- package/src/frameworks/detector.test.ts +248 -0
- package/src/frameworks/detector.ts +371 -0
- package/src/frameworks/index.ts +68 -0
- package/src/frameworks/recipes/angular.yaml +171 -0
- package/src/frameworks/recipes/astro.yaml +206 -0
- package/src/frameworks/recipes/django.yaml +180 -0
- package/src/frameworks/recipes/laravel.yaml +137 -0
- package/src/frameworks/recipes/nextjs.yaml +268 -0
- package/src/frameworks/recipes/nuxt.yaml +175 -0
- package/src/frameworks/recipes/rails.yaml +188 -0
- package/src/frameworks/recipes/react.yaml +202 -0
- package/src/frameworks/recipes/sveltekit.yaml +154 -0
- package/src/frameworks/recipes/vue.yaml +137 -0
- package/src/frameworks/recipes/wordpress.yaml +209 -0
- package/src/frameworks/suggestion-engine.ts +320 -0
- package/src/geo/geo-content.test.ts +305 -0
- package/src/geo/geo-content.ts +266 -0
- package/src/geo/geo-history.test.ts +473 -0
- package/src/geo/geo-history.ts +433 -0
- package/src/geo/geo-tracker.test.ts +359 -0
- package/src/geo/geo-tracker.ts +411 -0
- package/src/geo/index.ts +10 -0
- package/src/git/commit-helper.test.ts +261 -0
- package/src/git/commit-helper.ts +329 -0
- package/src/git/index.ts +12 -0
- package/src/git/pr-helper.test.ts +284 -0
- package/src/git/pr-helper.ts +307 -0
- package/src/index.ts +66 -0
- package/src/keywords/ai-keyword-engine.ts +1062 -0
- package/src/keywords/ai-summarizer.ts +387 -0
- package/src/keywords/ci-mode.ts +555 -0
- package/src/keywords/engine.ts +359 -0
- package/src/keywords/index.ts +151 -0
- package/src/keywords/llm-judge.ts +357 -0
- package/src/keywords/nlp-analysis.ts +706 -0
- package/src/keywords/prioritizer.ts +295 -0
- package/src/keywords/site-crawler.ts +342 -0
- package/src/keywords/sources/autocomplete.ts +139 -0
- package/src/keywords/sources/competitive-search.ts +450 -0
- package/src/keywords/sources/competitor-analysis.ts +374 -0
- package/src/keywords/sources/dataforseo.ts +206 -0
- package/src/keywords/sources/free-sources.ts +294 -0
- package/src/keywords/sources/gsc.ts +123 -0
- package/src/keywords/topic-grouping.ts +327 -0
- package/src/keywords/types.ts +144 -0
- package/src/keywords/wizard.ts +457 -0
- package/src/loader.ts +40 -0
- package/src/reports/index.ts +7 -0
- package/src/reports/report-generator.test.ts +293 -0
- package/src/reports/report-generator.ts +713 -0
- package/src/scheduler/alerts.test.ts +458 -0
- package/src/scheduler/alerts.ts +328 -0
- package/src/scheduler/index.ts +8 -0
- package/src/scheduler/scheduled-audit.test.ts +377 -0
- package/src/scheduler/scheduled-audit.ts +149 -0
- package/src/test/integration-test.ts +325 -0
- package/src/tools/analyzer.ts +373 -0
- package/src/tools/crawl.ts +293 -0
- package/src/tools/files.ts +301 -0
- package/src/tools/h1-fixer.ts +249 -0
- package/src/tools/index.ts +67 -0
- package/src/tracking/github-action.ts +326 -0
- package/src/tracking/google-analytics.ts +265 -0
- package/src/tracking/index.ts +45 -0
- package/src/tracking/report-generator.ts +386 -0
- package/src/tracking/search-console.ts +335 -0
- package/src/types.ts +134 -0
- package/src/utils/http.ts +302 -0
- package/src/wasm-adapter.ts +297 -0
- package/src/wasm-entry.ts +14 -0
- package/tsconfig.json +17 -0
- package/tsup.wasm.config.ts +26 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-Powered Keyword Research Engine
|
|
3
|
+
*
|
|
4
|
+
* Tiered access:
|
|
5
|
+
* - FREE: Basic keyword extraction + score teaser
|
|
6
|
+
* - PAID: Full NLP, clustering, embeddings, competitive analysis
|
|
7
|
+
*
|
|
8
|
+
* Based on Bhanu's "Engineering as Marketing" strategy:
|
|
9
|
+
* 1. Find low KD keywords (< 10)
|
|
10
|
+
* 2. High volume (1000+)
|
|
11
|
+
* 3. Relevant to product
|
|
12
|
+
* 4. Easy to build free tools for
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import OpenAI from 'openai';
|
|
16
|
+
import { crawlSite, extractKeyPhrases, type SiteCrawlResult } from './site-crawler.js';
|
|
17
|
+
import { summarizeSite, generateUncertaintyQuestions, type SiteSummary } from './ai-summarizer.js';
|
|
18
|
+
import {
|
|
19
|
+
runNLPAnalysis,
|
|
20
|
+
calculateTFIDF,
|
|
21
|
+
extractNgrams,
|
|
22
|
+
tokenize,
|
|
23
|
+
type NLPAnalysisResult,
|
|
24
|
+
} from './nlp-analysis.js';
|
|
25
|
+
import { getExpandedSuggestions, enrichKeywordsWithEstimates } from './sources/autocomplete.js';
|
|
26
|
+
import { getAllFreeKeywordIdeas, applyEstimates } from './sources/free-sources.js';
|
|
27
|
+
import type { KeywordData, SiteProfile } from './types.js';
|
|
28
|
+
import { prioritizeKeywords } from './prioritizer.js';
|
|
29
|
+
import {
|
|
30
|
+
searchCompetitors,
|
|
31
|
+
searchFormatConverters,
|
|
32
|
+
searchHackerNews,
|
|
33
|
+
type CompetitiveSearchResult,
|
|
34
|
+
type CompetitorTool,
|
|
35
|
+
type CompetitiveSearchOptions,
|
|
36
|
+
} from './sources/competitive-search.js';
|
|
37
|
+
import {
|
|
38
|
+
evaluateAndEnhanceToolIdeas,
|
|
39
|
+
type EnhancedToolIdea,
|
|
40
|
+
type LLMJudgeOptions,
|
|
41
|
+
} from './llm-judge.js';
|
|
42
|
+
|
|
43
|
+
export type PlanTier = 'free' | 'solo' | 'pro' | 'agency';
|
|
44
|
+
|
|
45
|
+
export interface AIKeywordResearchOptions {
|
|
46
|
+
url: string;
|
|
47
|
+
tier: PlanTier;
|
|
48
|
+
openaiApiKey?: string;
|
|
49
|
+
maxPages?: number;
|
|
50
|
+
userContext?: {
|
|
51
|
+
productDescription?: string;
|
|
52
|
+
targetAudience?: string;
|
|
53
|
+
competitors?: string[];
|
|
54
|
+
mainProblem?: string;
|
|
55
|
+
differentiator?: string;
|
|
56
|
+
};
|
|
57
|
+
wizardResponses?: Array<{ id: string; answer: string }>;
|
|
58
|
+
ciMode?: boolean;
|
|
59
|
+
/** API keys for competitive search (optional but recommended) */
|
|
60
|
+
competitiveSearchKeys?: {
|
|
61
|
+
braveApiKey?: string;
|
|
62
|
+
serperApiKey?: string;
|
|
63
|
+
githubToken?: string;
|
|
64
|
+
};
|
|
65
|
+
/** Enable enhanced tool ideas with LLM judge (default: true for paid tiers) */
|
|
66
|
+
enableEnhancedToolIdeas?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface FreeKeywordResult {
|
|
70
|
+
tier: 'free';
|
|
71
|
+
/** Basic keyword suggestions (limited) */
|
|
72
|
+
keywords: Array<{
|
|
73
|
+
keyword: string;
|
|
74
|
+
estimatedVolume: 'low' | 'medium' | 'high';
|
|
75
|
+
estimatedDifficulty: 'low' | 'medium' | 'high';
|
|
76
|
+
relevance: number;
|
|
77
|
+
}>;
|
|
78
|
+
/** Overall keyword opportunity score (teaser) */
|
|
79
|
+
opportunityScore: number;
|
|
80
|
+
/** Teaser message */
|
|
81
|
+
teaserMessage: string;
|
|
82
|
+
/** What they'd get with upgrade */
|
|
83
|
+
upgradeFeatures: string[];
|
|
84
|
+
/** Basic site understanding */
|
|
85
|
+
siteCategory: string;
|
|
86
|
+
/** Detected keywords count */
|
|
87
|
+
totalKeywordsFound: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface PaidKeywordResult {
|
|
91
|
+
tier: PlanTier;
|
|
92
|
+
/** Site summary from AI */
|
|
93
|
+
siteSummary: SiteSummary;
|
|
94
|
+
/** Full keyword opportunities */
|
|
95
|
+
opportunities: KeywordOpportunity[];
|
|
96
|
+
/** NLP analysis results */
|
|
97
|
+
nlpAnalysis: NLPAnalysisResult;
|
|
98
|
+
/** Keyword clusters */
|
|
99
|
+
clusters: Array<{
|
|
100
|
+
name: string;
|
|
101
|
+
keywords: string[];
|
|
102
|
+
totalVolume: number;
|
|
103
|
+
avgDifficulty: number;
|
|
104
|
+
}>;
|
|
105
|
+
/** Recommended free tools to build */
|
|
106
|
+
freeToolIdeas: FreeToolIdea[];
|
|
107
|
+
/** Competitive positioning */
|
|
108
|
+
competitiveInsights?: CompetitiveInsight[];
|
|
109
|
+
/** Uncertainty assessment */
|
|
110
|
+
uncertainty: UncertaintyAssessment;
|
|
111
|
+
/** Prioritized recommendations */
|
|
112
|
+
recommendations: KeywordRecommendation[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface KeywordOpportunity {
|
|
116
|
+
keyword: string;
|
|
117
|
+
volume: number;
|
|
118
|
+
difficulty: number;
|
|
119
|
+
relevance: number;
|
|
120
|
+
opportunityScore: number;
|
|
121
|
+
intent: 'informational' | 'commercial' | 'transactional' | 'navigational';
|
|
122
|
+
suggestedContentType: 'free-tool' | 'landing-page' | 'blog-post' | 'comparison' | 'guide';
|
|
123
|
+
suggestedCTA: string;
|
|
124
|
+
rationale: string;
|
|
125
|
+
cluster?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface FreeToolIdea {
|
|
129
|
+
keyword: string;
|
|
130
|
+
toolName: string;
|
|
131
|
+
toolDescription: string;
|
|
132
|
+
estimatedEffort: 'low' | 'medium' | 'high';
|
|
133
|
+
ctaConnection: string;
|
|
134
|
+
volume: number;
|
|
135
|
+
difficulty: number;
|
|
136
|
+
priority: number;
|
|
137
|
+
/** Enhanced fields from LLM judge */
|
|
138
|
+
inputFormat?: string;
|
|
139
|
+
outputFormat?: string;
|
|
140
|
+
productTieIn?: string;
|
|
141
|
+
ctaText?: string;
|
|
142
|
+
deepLinkPattern?: string;
|
|
143
|
+
competitors?: {
|
|
144
|
+
hasFreeAlternative: boolean;
|
|
145
|
+
topCompetitor?: string;
|
|
146
|
+
ourAdvantage: string;
|
|
147
|
+
};
|
|
148
|
+
implementationHints?: {
|
|
149
|
+
suggestedLibraries: string[];
|
|
150
|
+
estimatedComplexity: 'trivial' | 'simple' | 'moderate' | 'complex';
|
|
151
|
+
canUseExistingOSS: boolean;
|
|
152
|
+
ossLibrary?: string;
|
|
153
|
+
};
|
|
154
|
+
feasibilityScore?: {
|
|
155
|
+
feasibility: number;
|
|
156
|
+
specificity: number;
|
|
157
|
+
productFit: number;
|
|
158
|
+
marketOpportunity: number;
|
|
159
|
+
overallScore: number;
|
|
160
|
+
passes: boolean;
|
|
161
|
+
reasoning: string;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface CompetitiveInsight {
|
|
166
|
+
competitor: string;
|
|
167
|
+
theirFocus: 'broad' | 'niche';
|
|
168
|
+
ourAdvantage?: string;
|
|
169
|
+
keywordsTheyRankFor: string[];
|
|
170
|
+
keywordsWeCanTarget: string[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface UncertaintyAssessment {
|
|
174
|
+
overallConfidence: number;
|
|
175
|
+
siteUnderstanding: number;
|
|
176
|
+
audienceClarity: number;
|
|
177
|
+
competitiveClarity: number;
|
|
178
|
+
canAutomate: boolean;
|
|
179
|
+
questionsToAsk: Array<{
|
|
180
|
+
id: string;
|
|
181
|
+
question: string;
|
|
182
|
+
impact: number;
|
|
183
|
+
options?: string[];
|
|
184
|
+
}>;
|
|
185
|
+
explanation: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface KeywordRecommendation {
|
|
189
|
+
priority: number;
|
|
190
|
+
keyword: string;
|
|
191
|
+
action: 'build-tool' | 'create-page' | 'write-content' | 'skip' | 'needs-input';
|
|
192
|
+
effort: 'low' | 'medium' | 'high';
|
|
193
|
+
impact: 'low' | 'medium' | 'high';
|
|
194
|
+
rationale: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Run keyword research based on tier
|
|
199
|
+
*/
|
|
200
|
+
export async function runAIKeywordResearch(
|
|
201
|
+
options: AIKeywordResearchOptions
|
|
202
|
+
): Promise<FreeKeywordResult | PaidKeywordResult> {
|
|
203
|
+
const { url, tier, openaiApiKey, maxPages = 20 } = options;
|
|
204
|
+
|
|
205
|
+
console.log(`\n🔬 Starting AI Keyword Research (${tier} tier)...\n`);
|
|
206
|
+
|
|
207
|
+
// Step 1: Crawl the site
|
|
208
|
+
const crawlResult = await crawlSite(url, { maxPages });
|
|
209
|
+
|
|
210
|
+
// Route based on tier
|
|
211
|
+
if (tier === 'free') {
|
|
212
|
+
return runFreeKeywordResearch(crawlResult, options);
|
|
213
|
+
} else {
|
|
214
|
+
if (!openaiApiKey) {
|
|
215
|
+
throw new Error('OpenAI API key required for paid tier keyword research');
|
|
216
|
+
}
|
|
217
|
+
return runPaidKeywordResearch(crawlResult, options);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* FREE TIER: Basic keyword extraction with teaser
|
|
223
|
+
*/
|
|
224
|
+
async function runFreeKeywordResearch(
|
|
225
|
+
crawlResult: SiteCrawlResult,
|
|
226
|
+
options: AIKeywordResearchOptions
|
|
227
|
+
): Promise<FreeKeywordResult> {
|
|
228
|
+
console.log('📊 Running free tier analysis...');
|
|
229
|
+
|
|
230
|
+
// Basic TF-IDF analysis (no AI)
|
|
231
|
+
const documents = crawlResult.pages.map((p) => p.mainContent);
|
|
232
|
+
const tfidfResults = calculateTFIDF(documents);
|
|
233
|
+
|
|
234
|
+
// Extract key phrases
|
|
235
|
+
const keyPhrases = extractKeyPhrases(crawlResult);
|
|
236
|
+
|
|
237
|
+
// Get basic autocomplete suggestions for top terms
|
|
238
|
+
const topTerms = tfidfResults.slice(0, 5).map((r) => r.term);
|
|
239
|
+
let autocompleteKeywords: KeywordData[] = [];
|
|
240
|
+
|
|
241
|
+
for (const term of topTerms) {
|
|
242
|
+
try {
|
|
243
|
+
const suggestions = await getExpandedSuggestions(term, {});
|
|
244
|
+
autocompleteKeywords.push(...suggestions.slice(0, 5));
|
|
245
|
+
} catch {
|
|
246
|
+
// Ignore errors
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Apply volume estimates
|
|
251
|
+
autocompleteKeywords = applyEstimates(enrichKeywordsWithEstimates(autocompleteKeywords));
|
|
252
|
+
|
|
253
|
+
// Create limited results (teaser)
|
|
254
|
+
const limitedKeywords = autocompleteKeywords.slice(0, 10).map((kw) => {
|
|
255
|
+
const estimatedVolume: 'low' | 'medium' | 'high' = kw.searchVolume > 5000 ? 'high' : kw.searchVolume > 1000 ? 'medium' : 'low';
|
|
256
|
+
const estimatedDifficulty: 'low' | 'medium' | 'high' = kw.keywordDifficulty > 50 ? 'high' : kw.keywordDifficulty > 20 ? 'medium' : 'low';
|
|
257
|
+
return {
|
|
258
|
+
keyword: kw.keyword,
|
|
259
|
+
estimatedVolume,
|
|
260
|
+
estimatedDifficulty,
|
|
261
|
+
relevance: 0.5 + Math.random() * 0.5, // Simplified relevance
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Calculate opportunity score
|
|
266
|
+
const opportunityScore = Math.min(
|
|
267
|
+
100,
|
|
268
|
+
Math.round(
|
|
269
|
+
(limitedKeywords.filter((k) => k.estimatedDifficulty === 'low').length / limitedKeywords.length) * 50 +
|
|
270
|
+
(limitedKeywords.filter((k) => k.estimatedVolume !== 'low').length / limitedKeywords.length) * 50
|
|
271
|
+
)
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Detect site category
|
|
275
|
+
const siteCategory = detectSiteCategory(crawlResult, tfidfResults);
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
tier: 'free',
|
|
279
|
+
keywords: limitedKeywords,
|
|
280
|
+
opportunityScore,
|
|
281
|
+
totalKeywordsFound: autocompleteKeywords.length + keyPhrases.length,
|
|
282
|
+
siteCategory,
|
|
283
|
+
teaserMessage: `We found ${autocompleteKeywords.length + keyPhrases.length} potential keywords for ${crawlResult.domain}. Upgrade to see full analysis with AI-powered insights, keyword clustering, and competitive analysis.`,
|
|
284
|
+
upgradeFeatures: [
|
|
285
|
+
'🧠 AI-powered site analysis',
|
|
286
|
+
'📊 Full keyword data (volume, difficulty, trends)',
|
|
287
|
+
'🎯 Semantic keyword clustering',
|
|
288
|
+
'🛠️ Free tool ideas with CTA suggestions',
|
|
289
|
+
'🏆 Competitive positioning analysis',
|
|
290
|
+
'📈 Prioritized action recommendations',
|
|
291
|
+
'🤖 Uncertainty wizard for better results',
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* PAID TIER: Full AI-powered analysis
|
|
298
|
+
*/
|
|
299
|
+
async function runPaidKeywordResearch(
|
|
300
|
+
crawlResult: SiteCrawlResult,
|
|
301
|
+
options: AIKeywordResearchOptions
|
|
302
|
+
): Promise<PaidKeywordResult> {
|
|
303
|
+
const { openaiApiKey, userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
|
|
304
|
+
const enableEnhancedToolIdeas = options.enableEnhancedToolIdeas !== false; // Default true
|
|
305
|
+
|
|
306
|
+
console.log('🚀 Running full AI analysis...');
|
|
307
|
+
|
|
308
|
+
// Step 1: AI Site Summary
|
|
309
|
+
let siteSummary = await summarizeSite(crawlResult, {
|
|
310
|
+
openaiApiKey: openaiApiKey!,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Apply user context to improve summary
|
|
314
|
+
if (userContext) {
|
|
315
|
+
siteSummary = applyUserContext(siteSummary, userContext);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Apply wizard responses
|
|
319
|
+
if (wizardResponses) {
|
|
320
|
+
siteSummary = applyWizardResponses(siteSummary, wizardResponses);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Step 2: Full NLP Analysis
|
|
324
|
+
const documents = crawlResult.pages.map((p) => p.mainContent);
|
|
325
|
+
const nlpAnalysis = await runNLPAnalysis(documents, {
|
|
326
|
+
openaiApiKey,
|
|
327
|
+
numClusters: 8,
|
|
328
|
+
numTopics: 6,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Step 3: Generate seed keywords from multiple sources
|
|
332
|
+
const seedKeywords = generateSeedKeywords(siteSummary, nlpAnalysis, crawlResult);
|
|
333
|
+
|
|
334
|
+
// Step 4: Expand keywords with autocomplete and volume data
|
|
335
|
+
console.log('🔍 Expanding keyword opportunities...');
|
|
336
|
+
let allKeywords: KeywordData[] = [];
|
|
337
|
+
|
|
338
|
+
for (const seed of seedKeywords.slice(0, 15)) {
|
|
339
|
+
try {
|
|
340
|
+
const suggestions = await getExpandedSuggestions(seed, {});
|
|
341
|
+
allKeywords.push(...suggestions.slice(0, 20));
|
|
342
|
+
|
|
343
|
+
const freeIdeas = await getAllFreeKeywordIdeas(seed, {
|
|
344
|
+
includeQuestions: true,
|
|
345
|
+
includeModifiers: true,
|
|
346
|
+
});
|
|
347
|
+
allKeywords.push(...freeIdeas);
|
|
348
|
+
} catch {
|
|
349
|
+
// Continue with other seeds
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Enrich with volume estimates
|
|
354
|
+
allKeywords = applyEstimates(enrichKeywordsWithEstimates(allKeywords));
|
|
355
|
+
|
|
356
|
+
// Deduplicate
|
|
357
|
+
const uniqueKeywords = deduplicateKeywords(allKeywords);
|
|
358
|
+
|
|
359
|
+
// Step 5: Score and prioritize
|
|
360
|
+
const opportunities = scoreKeywordOpportunities(
|
|
361
|
+
uniqueKeywords,
|
|
362
|
+
siteSummary,
|
|
363
|
+
nlpAnalysis
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// Step 6: Generate free tool ideas with competitive research and LLM judge
|
|
367
|
+
let freeToolIdeas: FreeToolIdea[];
|
|
368
|
+
|
|
369
|
+
if (enableEnhancedToolIdeas && openaiApiKey) {
|
|
370
|
+
console.log('🔬 Running enhanced tool idea analysis with competitive research...');
|
|
371
|
+
freeToolIdeas = await generateEnhancedFreeToolIdeas(
|
|
372
|
+
opportunities,
|
|
373
|
+
siteSummary,
|
|
374
|
+
{
|
|
375
|
+
openaiApiKey,
|
|
376
|
+
...competitiveSearchKeys,
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
} else {
|
|
380
|
+
// Fallback to basic tool idea generation
|
|
381
|
+
freeToolIdeas = generateBasicFreeToolIdeas(opportunities, siteSummary);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Step 7: Create keyword clusters
|
|
385
|
+
const clusters = createKeywordClusters(opportunities, nlpAnalysis);
|
|
386
|
+
|
|
387
|
+
// Step 8: Assess uncertainty
|
|
388
|
+
const uncertainty = assessUncertainty(siteSummary, opportunities, ciMode);
|
|
389
|
+
|
|
390
|
+
// Step 9: Generate recommendations
|
|
391
|
+
const recommendations = generateRecommendations(
|
|
392
|
+
opportunities,
|
|
393
|
+
freeToolIdeas,
|
|
394
|
+
uncertainty
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
tier: options.tier,
|
|
399
|
+
siteSummary,
|
|
400
|
+
opportunities: opportunities.slice(0, 100),
|
|
401
|
+
nlpAnalysis,
|
|
402
|
+
clusters,
|
|
403
|
+
freeToolIdeas: freeToolIdeas.slice(0, 20),
|
|
404
|
+
uncertainty,
|
|
405
|
+
recommendations: recommendations.slice(0, 15),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Generate seed keywords from AI analysis
|
|
411
|
+
*/
|
|
412
|
+
function generateSeedKeywords(
|
|
413
|
+
summary: SiteSummary,
|
|
414
|
+
nlp: NLPAnalysisResult,
|
|
415
|
+
crawl: SiteCrawlResult
|
|
416
|
+
): string[] {
|
|
417
|
+
const seeds = new Set<string>();
|
|
418
|
+
|
|
419
|
+
// From AI summary
|
|
420
|
+
summary.suggestedSeedKeywords.forEach((k) => seeds.add(k.toLowerCase()));
|
|
421
|
+
|
|
422
|
+
// From key features
|
|
423
|
+
summary.keyFeatures.forEach((f) => {
|
|
424
|
+
const words = tokenize(f);
|
|
425
|
+
if (words.length <= 3) {
|
|
426
|
+
seeds.add(words.join(' '));
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// From problems solved
|
|
431
|
+
summary.problemsSolved.forEach((p) => {
|
|
432
|
+
const words = tokenize(p);
|
|
433
|
+
if (words.length <= 4) {
|
|
434
|
+
seeds.add(words.join(' '));
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// From TF-IDF
|
|
439
|
+
nlp.tfidfKeywords.slice(0, 15).forEach((k) => seeds.add(k.term));
|
|
440
|
+
|
|
441
|
+
// From bigrams
|
|
442
|
+
nlp.ngrams.bigrams.slice(0, 10).forEach((g) => seeds.add(g.phrase));
|
|
443
|
+
|
|
444
|
+
// From entity phrases
|
|
445
|
+
nlp.entityPhrases.slice(0, 10).forEach((e) => seeds.add(e.toLowerCase()));
|
|
446
|
+
|
|
447
|
+
// Product name variations
|
|
448
|
+
const productName = summary.productName.toLowerCase();
|
|
449
|
+
seeds.add(productName);
|
|
450
|
+
seeds.add(`${productName} alternative`);
|
|
451
|
+
seeds.add(`${productName} vs`);
|
|
452
|
+
seeds.add(`best ${summary.industry.toLowerCase()}`);
|
|
453
|
+
|
|
454
|
+
// Industry + modifiers
|
|
455
|
+
const industry = summary.industry.toLowerCase();
|
|
456
|
+
seeds.add(`${industry} tool`);
|
|
457
|
+
seeds.add(`${industry} software`);
|
|
458
|
+
seeds.add(`free ${industry}`);
|
|
459
|
+
seeds.add(`${industry} generator`);
|
|
460
|
+
seeds.add(`${industry} ai`);
|
|
461
|
+
|
|
462
|
+
return Array.from(seeds).filter((s) => s.length > 2 && s.length < 50);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Score keyword opportunities
|
|
467
|
+
*/
|
|
468
|
+
function scoreKeywordOpportunities(
|
|
469
|
+
keywords: KeywordData[],
|
|
470
|
+
summary: SiteSummary,
|
|
471
|
+
nlp: NLPAnalysisResult
|
|
472
|
+
): KeywordOpportunity[] {
|
|
473
|
+
const opportunities: KeywordOpportunity[] = [];
|
|
474
|
+
|
|
475
|
+
// Get relevance terms from summary
|
|
476
|
+
const relevanceTerms = new Set([
|
|
477
|
+
...summary.suggestedSeedKeywords.map((k) => k.toLowerCase()),
|
|
478
|
+
...summary.keyFeatures.flatMap(tokenize),
|
|
479
|
+
...summary.problemsSolved.flatMap(tokenize),
|
|
480
|
+
summary.industry.toLowerCase(),
|
|
481
|
+
summary.productName.toLowerCase(),
|
|
482
|
+
]);
|
|
483
|
+
|
|
484
|
+
for (const kw of keywords) {
|
|
485
|
+
// Calculate relevance
|
|
486
|
+
const kwTokens = tokenize(kw.keyword);
|
|
487
|
+
const relevance = kwTokens.filter((t) => relevanceTerms.has(t)).length / Math.max(kwTokens.length, 1);
|
|
488
|
+
|
|
489
|
+
// Skip very low relevance
|
|
490
|
+
if (relevance < 0.1 && kw.searchVolume < 500) continue;
|
|
491
|
+
|
|
492
|
+
// Calculate opportunity score
|
|
493
|
+
// Higher volume + lower difficulty + higher relevance = better opportunity
|
|
494
|
+
const volumeScore = Math.min(1, Math.log10(kw.searchVolume + 1) / 4);
|
|
495
|
+
const difficultyScore = 1 - kw.keywordDifficulty / 100;
|
|
496
|
+
const opportunityScore = Math.round(
|
|
497
|
+
(volumeScore * 0.3 + difficultyScore * 0.4 + relevance * 0.3) * 100
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
// Determine intent
|
|
501
|
+
const intent = determineIntent(kw.keyword);
|
|
502
|
+
|
|
503
|
+
// Suggest content type
|
|
504
|
+
const suggestedContentType = suggestContentType(kw.keyword, kw.keywordDifficulty, intent);
|
|
505
|
+
|
|
506
|
+
// Generate CTA
|
|
507
|
+
const suggestedCTA = generateCTA(kw.keyword, summary);
|
|
508
|
+
|
|
509
|
+
// Generate rationale
|
|
510
|
+
const rationale = generateRationale(kw, relevance, summary);
|
|
511
|
+
|
|
512
|
+
opportunities.push({
|
|
513
|
+
keyword: kw.keyword,
|
|
514
|
+
volume: kw.searchVolume,
|
|
515
|
+
difficulty: kw.keywordDifficulty,
|
|
516
|
+
relevance: Math.round(relevance * 100),
|
|
517
|
+
opportunityScore,
|
|
518
|
+
intent,
|
|
519
|
+
suggestedContentType,
|
|
520
|
+
suggestedCTA,
|
|
521
|
+
rationale,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Sort by opportunity score
|
|
526
|
+
return opportunities.sort((a, b) => b.opportunityScore - a.opportunityScore);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Generate free tool ideas with competitive research and LLM judge (enhanced version)
|
|
531
|
+
*/
|
|
532
|
+
async function generateEnhancedFreeToolIdeas(
|
|
533
|
+
opportunities: KeywordOpportunity[],
|
|
534
|
+
summary: SiteSummary,
|
|
535
|
+
options: {
|
|
536
|
+
openaiApiKey: string;
|
|
537
|
+
braveApiKey?: string;
|
|
538
|
+
serperApiKey?: string;
|
|
539
|
+
githubToken?: string;
|
|
540
|
+
}
|
|
541
|
+
): Promise<FreeToolIdea[]> {
|
|
542
|
+
// Filter for tool-worthy keywords (low difficulty, decent volume)
|
|
543
|
+
const toolKeywords = opportunities.filter(
|
|
544
|
+
(kw) =>
|
|
545
|
+
kw.difficulty < 30 && // Slightly more lenient
|
|
546
|
+
kw.volume >= 300 && // Slightly lower threshold
|
|
547
|
+
(kw.keyword.includes('generator') ||
|
|
548
|
+
kw.keyword.includes('tool') ||
|
|
549
|
+
kw.keyword.includes('maker') ||
|
|
550
|
+
kw.keyword.includes('builder') ||
|
|
551
|
+
kw.keyword.includes('converter') ||
|
|
552
|
+
kw.keyword.includes('calculator') ||
|
|
553
|
+
kw.keyword.includes('checker') ||
|
|
554
|
+
kw.keyword.includes('validator') ||
|
|
555
|
+
kw.keyword.includes('formatter') ||
|
|
556
|
+
kw.keyword.includes('free') ||
|
|
557
|
+
kw.intent === 'informational')
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
// Prepare basic tool ideas for enhancement
|
|
561
|
+
const basicIdeas = toolKeywords.slice(0, 15).map((kw) => ({
|
|
562
|
+
keyword: kw.keyword,
|
|
563
|
+
toolName: generateToolName(kw.keyword),
|
|
564
|
+
toolDescription: generateToolDescription(kw.keyword, summary),
|
|
565
|
+
volume: kw.volume,
|
|
566
|
+
difficulty: kw.difficulty,
|
|
567
|
+
}));
|
|
568
|
+
|
|
569
|
+
// Create competitive search function with caching
|
|
570
|
+
const competitiveCache = new Map<string, CompetitiveSearchResult>();
|
|
571
|
+
|
|
572
|
+
const getCompetitiveData = async (keyword: string): Promise<CompetitiveSearchResult | null> => {
|
|
573
|
+
// Check cache
|
|
574
|
+
if (competitiveCache.has(keyword)) {
|
|
575
|
+
return competitiveCache.get(keyword)!;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const result = await searchCompetitors(keyword, {
|
|
580
|
+
braveApiKey: options.braveApiKey,
|
|
581
|
+
serperApiKey: options.serperApiKey,
|
|
582
|
+
githubToken: options.githubToken,
|
|
583
|
+
maxResults: 5,
|
|
584
|
+
});
|
|
585
|
+
competitiveCache.set(keyword, result);
|
|
586
|
+
return result;
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.warn(`Competitive search failed for "${keyword}":`, error);
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// Evaluate and enhance tool ideas using LLM judge
|
|
594
|
+
const enhancedIdeas = await evaluateAndEnhanceToolIdeas(
|
|
595
|
+
basicIdeas,
|
|
596
|
+
summary,
|
|
597
|
+
getCompetitiveData,
|
|
598
|
+
{ openaiApiKey: options.openaiApiKey }
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
// Convert enhanced ideas to FreeToolIdea format
|
|
602
|
+
return enhancedIdeas.map((enhanced) => ({
|
|
603
|
+
keyword: enhanced.keyword,
|
|
604
|
+
toolName: enhanced.toolName,
|
|
605
|
+
toolDescription: enhanced.description,
|
|
606
|
+
estimatedEffort: complexityToEffort(enhanced.implementationHints.estimatedComplexity),
|
|
607
|
+
ctaConnection: enhanced.ctaText,
|
|
608
|
+
volume: enhanced.volume,
|
|
609
|
+
difficulty: enhanced.difficulty,
|
|
610
|
+
priority: enhanced.priority,
|
|
611
|
+
inputFormat: enhanced.inputFormat,
|
|
612
|
+
outputFormat: enhanced.outputFormat,
|
|
613
|
+
productTieIn: enhanced.productTieIn,
|
|
614
|
+
ctaText: enhanced.ctaText,
|
|
615
|
+
deepLinkPattern: enhanced.deepLinkPattern,
|
|
616
|
+
competitors: enhanced.competitors,
|
|
617
|
+
implementationHints: enhanced.implementationHints,
|
|
618
|
+
feasibilityScore: {
|
|
619
|
+
feasibility: enhanced.feasibilityScore.feasibility,
|
|
620
|
+
specificity: enhanced.feasibilityScore.specificity,
|
|
621
|
+
productFit: enhanced.feasibilityScore.productFit,
|
|
622
|
+
marketOpportunity: enhanced.feasibilityScore.marketOpportunity,
|
|
623
|
+
overallScore: enhanced.feasibilityScore.overallScore,
|
|
624
|
+
passes: enhanced.feasibilityScore.passes,
|
|
625
|
+
reasoning: enhanced.feasibilityScore.reasoning,
|
|
626
|
+
},
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Convert complexity to effort level
|
|
632
|
+
*/
|
|
633
|
+
function complexityToEffort(complexity: 'trivial' | 'simple' | 'moderate' | 'complex'): 'low' | 'medium' | 'high' {
|
|
634
|
+
switch (complexity) {
|
|
635
|
+
case 'trivial':
|
|
636
|
+
case 'simple':
|
|
637
|
+
return 'low';
|
|
638
|
+
case 'moderate':
|
|
639
|
+
return 'medium';
|
|
640
|
+
case 'complex':
|
|
641
|
+
return 'high';
|
|
642
|
+
default:
|
|
643
|
+
return 'medium';
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Generate free tool ideas (basic version - fallback)
|
|
649
|
+
*/
|
|
650
|
+
function generateBasicFreeToolIdeas(
|
|
651
|
+
opportunities: KeywordOpportunity[],
|
|
652
|
+
summary: SiteSummary
|
|
653
|
+
): FreeToolIdea[] {
|
|
654
|
+
const toolIdeas: FreeToolIdea[] = [];
|
|
655
|
+
|
|
656
|
+
// Filter for tool-worthy keywords (low difficulty, decent volume)
|
|
657
|
+
const toolKeywords = opportunities.filter(
|
|
658
|
+
(kw) =>
|
|
659
|
+
kw.difficulty < 20 &&
|
|
660
|
+
kw.volume >= 500 &&
|
|
661
|
+
(kw.keyword.includes('generator') ||
|
|
662
|
+
kw.keyword.includes('tool') ||
|
|
663
|
+
kw.keyword.includes('maker') ||
|
|
664
|
+
kw.keyword.includes('builder') ||
|
|
665
|
+
kw.keyword.includes('converter') ||
|
|
666
|
+
kw.keyword.includes('calculator') ||
|
|
667
|
+
kw.keyword.includes('checker') ||
|
|
668
|
+
kw.keyword.includes('free') ||
|
|
669
|
+
kw.intent === 'informational')
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
for (const kw of toolKeywords.slice(0, 30)) {
|
|
673
|
+
const toolName = generateToolName(kw.keyword);
|
|
674
|
+
const toolDescription = generateToolDescription(kw.keyword, summary);
|
|
675
|
+
const effort = estimateToolEffort(kw.keyword);
|
|
676
|
+
const ctaConnection = generateToolCTA(kw.keyword, summary);
|
|
677
|
+
|
|
678
|
+
// Calculate priority
|
|
679
|
+
const priority = Math.round(
|
|
680
|
+
(kw.volume / 10000) * 30 + // Volume contribution
|
|
681
|
+
((100 - kw.difficulty) / 100) * 40 + // Difficulty contribution
|
|
682
|
+
(kw.relevance / 100) * 30 // Relevance contribution
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
toolIdeas.push({
|
|
686
|
+
keyword: kw.keyword,
|
|
687
|
+
toolName,
|
|
688
|
+
toolDescription,
|
|
689
|
+
estimatedEffort: effort,
|
|
690
|
+
ctaConnection,
|
|
691
|
+
volume: kw.volume,
|
|
692
|
+
difficulty: kw.difficulty,
|
|
693
|
+
priority,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return toolIdeas.sort((a, b) => b.priority - a.priority);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Create keyword clusters
|
|
702
|
+
*/
|
|
703
|
+
function createKeywordClusters(
|
|
704
|
+
opportunities: KeywordOpportunity[],
|
|
705
|
+
nlp: NLPAnalysisResult
|
|
706
|
+
): Array<{
|
|
707
|
+
name: string;
|
|
708
|
+
keywords: string[];
|
|
709
|
+
totalVolume: number;
|
|
710
|
+
avgDifficulty: number;
|
|
711
|
+
}> {
|
|
712
|
+
// Use NLP clusters if available
|
|
713
|
+
if (nlp.clusters.length > 0) {
|
|
714
|
+
return nlp.clusters.map((cluster) => {
|
|
715
|
+
const clusterOpps = opportunities.filter((o) =>
|
|
716
|
+
cluster.keywords.some(
|
|
717
|
+
(k) => o.keyword.includes(k) || k.includes(o.keyword.split(' ')[0])
|
|
718
|
+
)
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
name: cluster.name,
|
|
723
|
+
keywords: clusterOpps.map((o) => o.keyword),
|
|
724
|
+
totalVolume: clusterOpps.reduce((sum, o) => sum + o.volume, 0),
|
|
725
|
+
avgDifficulty:
|
|
726
|
+
clusterOpps.length > 0
|
|
727
|
+
? Math.round(clusterOpps.reduce((sum, o) => sum + o.difficulty, 0) / clusterOpps.length)
|
|
728
|
+
: 0,
|
|
729
|
+
};
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Fallback: group by common terms
|
|
734
|
+
const clusters: Map<string, KeywordOpportunity[]> = new Map();
|
|
735
|
+
|
|
736
|
+
for (const opp of opportunities) {
|
|
737
|
+
const mainTerm = opp.keyword.split(' ')[0];
|
|
738
|
+
if (!clusters.has(mainTerm)) {
|
|
739
|
+
clusters.set(mainTerm, []);
|
|
740
|
+
}
|
|
741
|
+
clusters.get(mainTerm)!.push(opp);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return Array.from(clusters.entries())
|
|
745
|
+
.filter(([_, opps]) => opps.length >= 2)
|
|
746
|
+
.map(([name, opps]) => ({
|
|
747
|
+
name,
|
|
748
|
+
keywords: opps.map((o) => o.keyword),
|
|
749
|
+
totalVolume: opps.reduce((sum, o) => sum + o.volume, 0),
|
|
750
|
+
avgDifficulty: Math.round(opps.reduce((sum, o) => sum + o.difficulty, 0) / opps.length),
|
|
751
|
+
}))
|
|
752
|
+
.sort((a, b) => b.totalVolume - a.totalVolume)
|
|
753
|
+
.slice(0, 10);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Assess uncertainty for CI mode
|
|
758
|
+
*/
|
|
759
|
+
function assessUncertainty(
|
|
760
|
+
summary: SiteSummary,
|
|
761
|
+
opportunities: KeywordOpportunity[],
|
|
762
|
+
ciMode?: boolean
|
|
763
|
+
): UncertaintyAssessment {
|
|
764
|
+
const siteUnderstanding = summary.confidence;
|
|
765
|
+
const audienceClarity = summary.targetAudience.includes('Unknown') ? 0.3 : 0.8;
|
|
766
|
+
const competitiveClarity = 0.5; // Would be higher with competitor analysis
|
|
767
|
+
|
|
768
|
+
// Average relevance of top keywords
|
|
769
|
+
const topOpps = opportunities.slice(0, 20);
|
|
770
|
+
const avgRelevance = topOpps.length > 0
|
|
771
|
+
? topOpps.reduce((sum, o) => sum + o.relevance, 0) / topOpps.length / 100
|
|
772
|
+
: 0.5;
|
|
773
|
+
|
|
774
|
+
const overallConfidence =
|
|
775
|
+
(siteUnderstanding + audienceClarity + competitiveClarity + avgRelevance) / 4;
|
|
776
|
+
|
|
777
|
+
// In CI mode, require higher confidence
|
|
778
|
+
const threshold = ciMode ? 0.7 : 0.5;
|
|
779
|
+
const canAutomate = overallConfidence >= threshold;
|
|
780
|
+
|
|
781
|
+
// Generate questions to reduce uncertainty
|
|
782
|
+
const questions = generateUncertaintyQuestions(summary);
|
|
783
|
+
|
|
784
|
+
let explanation: string;
|
|
785
|
+
if (canAutomate) {
|
|
786
|
+
explanation = `Confidence is ${Math.round(overallConfidence * 100)}%. Keyword recommendations can be automated.`;
|
|
787
|
+
} else {
|
|
788
|
+
explanation = `Confidence is only ${Math.round(overallConfidence * 100)}%. ${
|
|
789
|
+
ciMode
|
|
790
|
+
? 'CI mode requires 70% confidence. Please run the keyword wizard to provide more context.'
|
|
791
|
+
: 'Consider running the keyword wizard for better results.'
|
|
792
|
+
}`;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
overallConfidence,
|
|
797
|
+
siteUnderstanding,
|
|
798
|
+
audienceClarity,
|
|
799
|
+
competitiveClarity,
|
|
800
|
+
canAutomate,
|
|
801
|
+
questionsToAsk: questions,
|
|
802
|
+
explanation,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Generate prioritized recommendations
|
|
808
|
+
*/
|
|
809
|
+
function generateRecommendations(
|
|
810
|
+
opportunities: KeywordOpportunity[],
|
|
811
|
+
toolIdeas: FreeToolIdea[],
|
|
812
|
+
uncertainty: UncertaintyAssessment
|
|
813
|
+
): KeywordRecommendation[] {
|
|
814
|
+
const recommendations: KeywordRecommendation[] = [];
|
|
815
|
+
let priority = 1;
|
|
816
|
+
|
|
817
|
+
// If uncertainty is high, first recommendation is to run wizard
|
|
818
|
+
if (!uncertainty.canAutomate) {
|
|
819
|
+
recommendations.push({
|
|
820
|
+
priority: priority++,
|
|
821
|
+
keyword: '',
|
|
822
|
+
action: 'needs-input',
|
|
823
|
+
effort: 'low',
|
|
824
|
+
impact: 'high',
|
|
825
|
+
rationale: `Run the keyword wizard to answer ${uncertainty.questionsToAsk.length} questions and improve recommendation accuracy.`,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Add top free tool ideas
|
|
830
|
+
for (const tool of toolIdeas.slice(0, 5)) {
|
|
831
|
+
recommendations.push({
|
|
832
|
+
priority: priority++,
|
|
833
|
+
keyword: tool.keyword,
|
|
834
|
+
action: 'build-tool',
|
|
835
|
+
effort: tool.estimatedEffort,
|
|
836
|
+
impact: tool.volume > 2000 ? 'high' : tool.volume > 500 ? 'medium' : 'low',
|
|
837
|
+
rationale: `Build "${tool.toolName}": ${tool.volume} monthly searches, KD ${tool.difficulty}. ${tool.ctaConnection}`,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Add top content opportunities
|
|
842
|
+
const contentOpps = opportunities
|
|
843
|
+
.filter((o) => o.suggestedContentType !== 'free-tool' && o.difficulty < 30)
|
|
844
|
+
.slice(0, 5);
|
|
845
|
+
|
|
846
|
+
for (const opp of contentOpps) {
|
|
847
|
+
recommendations.push({
|
|
848
|
+
priority: priority++,
|
|
849
|
+
keyword: opp.keyword,
|
|
850
|
+
action: opp.suggestedContentType === 'landing-page' ? 'create-page' : 'write-content',
|
|
851
|
+
effort: opp.suggestedContentType === 'guide' ? 'high' : 'medium',
|
|
852
|
+
impact: opp.volume > 2000 ? 'high' : opp.volume > 500 ? 'medium' : 'low',
|
|
853
|
+
rationale: opp.rationale,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return recommendations;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Helper functions
|
|
861
|
+
|
|
862
|
+
function detectSiteCategory(
|
|
863
|
+
crawl: SiteCrawlResult,
|
|
864
|
+
tfidf: Array<{ term: string; tfidf: number }>
|
|
865
|
+
): string {
|
|
866
|
+
const topTerms = tfidf.slice(0, 20).map((t) => t.term);
|
|
867
|
+
const text = topTerms.join(' ');
|
|
868
|
+
|
|
869
|
+
if (text.includes('saas') || text.includes('software') || text.includes('app')) {
|
|
870
|
+
return 'SaaS/Software';
|
|
871
|
+
}
|
|
872
|
+
if (text.includes('shop') || text.includes('buy') || text.includes('product')) {
|
|
873
|
+
return 'E-commerce';
|
|
874
|
+
}
|
|
875
|
+
if (text.includes('blog') || text.includes('article') || text.includes('news')) {
|
|
876
|
+
return 'Content/Media';
|
|
877
|
+
}
|
|
878
|
+
if (text.includes('agency') || text.includes('service') || text.includes('consult')) {
|
|
879
|
+
return 'Agency/Services';
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return 'General';
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function deduplicateKeywords(keywords: KeywordData[]): KeywordData[] {
|
|
886
|
+
const seen = new Map<string, KeywordData>();
|
|
887
|
+
for (const kw of keywords) {
|
|
888
|
+
const key = kw.keyword.toLowerCase().trim();
|
|
889
|
+
const existing = seen.get(key);
|
|
890
|
+
if (!existing || kw.searchVolume > existing.searchVolume) {
|
|
891
|
+
seen.set(key, kw);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return Array.from(seen.values());
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function determineIntent(keyword: string): 'informational' | 'commercial' | 'transactional' | 'navigational' {
|
|
898
|
+
const kw = keyword.toLowerCase();
|
|
899
|
+
if (kw.includes('how to') || kw.includes('what is') || kw.includes('guide') || kw.includes('tutorial')) {
|
|
900
|
+
return 'informational';
|
|
901
|
+
}
|
|
902
|
+
if (kw.includes('buy') || kw.includes('price') || kw.includes('cost') || kw.includes('purchase')) {
|
|
903
|
+
return 'transactional';
|
|
904
|
+
}
|
|
905
|
+
if (kw.includes('best') || kw.includes('top') || kw.includes('review') || kw.includes('vs')) {
|
|
906
|
+
return 'commercial';
|
|
907
|
+
}
|
|
908
|
+
return 'informational';
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function suggestContentType(
|
|
912
|
+
keyword: string,
|
|
913
|
+
difficulty: number,
|
|
914
|
+
intent: string
|
|
915
|
+
): 'free-tool' | 'landing-page' | 'blog-post' | 'comparison' | 'guide' {
|
|
916
|
+
const kw = keyword.toLowerCase();
|
|
917
|
+
|
|
918
|
+
if (kw.includes('generator') || kw.includes('tool') || kw.includes('maker') || kw.includes('calculator')) {
|
|
919
|
+
return 'free-tool';
|
|
920
|
+
}
|
|
921
|
+
if (kw.includes('vs') || kw.includes('alternative') || kw.includes('comparison')) {
|
|
922
|
+
return 'comparison';
|
|
923
|
+
}
|
|
924
|
+
if (intent === 'commercial' && difficulty < 30) {
|
|
925
|
+
return 'landing-page';
|
|
926
|
+
}
|
|
927
|
+
if (kw.includes('guide') || kw.includes('how to') || kw.includes('tutorial')) {
|
|
928
|
+
return 'guide';
|
|
929
|
+
}
|
|
930
|
+
return 'blog-post';
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function generateCTA(keyword: string, summary: SiteSummary): string {
|
|
934
|
+
const product = summary.productName;
|
|
935
|
+
const kw = keyword.toLowerCase();
|
|
936
|
+
|
|
937
|
+
if (kw.includes('generator') || kw.includes('tool')) {
|
|
938
|
+
return `Try ${product} for more advanced ${summary.industry.toLowerCase()} features →`;
|
|
939
|
+
}
|
|
940
|
+
if (kw.includes('free')) {
|
|
941
|
+
return `${product} offers a free tier with even more capabilities →`;
|
|
942
|
+
}
|
|
943
|
+
return `Discover how ${product} can help with ${kw.split(' ').slice(0, 3).join(' ')} →`;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function generateRationale(kw: KeywordData, relevance: number, summary: SiteSummary): string {
|
|
947
|
+
const parts: string[] = [];
|
|
948
|
+
|
|
949
|
+
if (kw.keywordDifficulty < 20) {
|
|
950
|
+
parts.push('Low competition - easy to rank');
|
|
951
|
+
} else if (kw.keywordDifficulty < 40) {
|
|
952
|
+
parts.push('Moderate competition');
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (kw.searchVolume > 5000) {
|
|
956
|
+
parts.push('High search volume');
|
|
957
|
+
} else if (kw.searchVolume > 1000) {
|
|
958
|
+
parts.push('Decent search volume');
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (relevance > 0.7) {
|
|
962
|
+
parts.push('Highly relevant to your product');
|
|
963
|
+
} else if (relevance > 0.4) {
|
|
964
|
+
parts.push('Related to your industry');
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
return parts.join('. ') + '.';
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function generateToolName(keyword: string): string {
|
|
971
|
+
const kw = keyword.toLowerCase();
|
|
972
|
+
const words = kw.split(' ').map((w) => w.charAt(0).toUpperCase() + w.slice(1));
|
|
973
|
+
|
|
974
|
+
if (kw.includes('generator')) {
|
|
975
|
+
return words.join(' ');
|
|
976
|
+
}
|
|
977
|
+
if (kw.includes('checker') || kw.includes('validator')) {
|
|
978
|
+
return words.join(' ');
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return `Free ${words.join(' ')} Tool`;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function generateToolDescription(keyword: string, summary: SiteSummary): string {
|
|
985
|
+
return `A free ${keyword} tool. Use it to ${keyword.replace('generator', 'generate').replace('checker', 'check')}. Try ${summary.productName} for more advanced features.`;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function estimateToolEffort(keyword: string): 'low' | 'medium' | 'high' {
|
|
989
|
+
const kw = keyword.toLowerCase();
|
|
990
|
+
|
|
991
|
+
if (kw.includes('calculator') || kw.includes('converter') || kw.includes('counter')) {
|
|
992
|
+
return 'low';
|
|
993
|
+
}
|
|
994
|
+
if (kw.includes('generator') || kw.includes('maker')) {
|
|
995
|
+
return 'medium';
|
|
996
|
+
}
|
|
997
|
+
if (kw.includes('analyzer') || kw.includes('checker') || kw.includes('builder')) {
|
|
998
|
+
return 'high';
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return 'medium';
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function generateToolCTA(keyword: string, summary: SiteSummary): string {
|
|
1005
|
+
return `"Like this free tool? ${summary.productName} can do much more - ${summary.valueProposition || `try ${summary.productName} free`}"`;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function applyUserContext(summary: SiteSummary, context: AIKeywordResearchOptions['userContext']): SiteSummary {
|
|
1009
|
+
if (!context) return summary;
|
|
1010
|
+
|
|
1011
|
+
return {
|
|
1012
|
+
...summary,
|
|
1013
|
+
productDescription: context.productDescription || summary.productDescription,
|
|
1014
|
+
targetAudience: context.targetAudience || summary.targetAudience,
|
|
1015
|
+
problemsSolved: context.mainProblem
|
|
1016
|
+
? [context.mainProblem, ...summary.problemsSolved]
|
|
1017
|
+
: summary.problemsSolved,
|
|
1018
|
+
nicheFocus: context.differentiator || summary.nicheFocus,
|
|
1019
|
+
confidence: Math.min(1, summary.confidence + 0.2), // Boost confidence with user input
|
|
1020
|
+
uncertainties: summary.uncertainties.filter(
|
|
1021
|
+
(u) =>
|
|
1022
|
+
!(context.targetAudience && u.includes('audience')) &&
|
|
1023
|
+
!(context.mainProblem && u.includes('problem'))
|
|
1024
|
+
),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function applyWizardResponses(
|
|
1029
|
+
summary: SiteSummary,
|
|
1030
|
+
responses: Array<{ id: string; answer: string }>
|
|
1031
|
+
): SiteSummary {
|
|
1032
|
+
let updated = { ...summary };
|
|
1033
|
+
let confidenceBoost = 0;
|
|
1034
|
+
|
|
1035
|
+
for (const response of responses) {
|
|
1036
|
+
switch (response.id) {
|
|
1037
|
+
case 'target_audience':
|
|
1038
|
+
updated.targetAudience = response.answer;
|
|
1039
|
+
confidenceBoost += 0.15;
|
|
1040
|
+
break;
|
|
1041
|
+
case 'industry':
|
|
1042
|
+
updated.industry = response.answer;
|
|
1043
|
+
confidenceBoost += 0.1;
|
|
1044
|
+
break;
|
|
1045
|
+
case 'main_problem':
|
|
1046
|
+
updated.problemsSolved = [response.answer, ...updated.problemsSolved];
|
|
1047
|
+
confidenceBoost += 0.15;
|
|
1048
|
+
break;
|
|
1049
|
+
case 'differentiator':
|
|
1050
|
+
updated.nicheFocus = response.answer;
|
|
1051
|
+
confidenceBoost += 0.1;
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
updated.confidence = Math.min(1, updated.confidence + confidenceBoost);
|
|
1057
|
+
updated.uncertainties = updated.uncertainties.filter(
|
|
1058
|
+
(u) => !responses.some((r) => u.toLowerCase().includes(r.id.replace('_', ' ')))
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
return updated;
|
|
1062
|
+
}
|