@rankcli/agent-runtime 0.0.15 → 0.0.16

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.
Files changed (202) hide show
  1. package/dist/index.d.mts +43 -393
  2. package/dist/index.d.ts +43 -393
  3. package/dist/index.js +306 -6526
  4. package/dist/index.mjs +202 -6412
  5. package/package.json +6 -1
  6. package/scripts/build-deno.ts +0 -134
  7. package/src/analyzers/core-web-vitals-analyzer.test.ts +0 -236
  8. package/src/analyzers/core-web-vitals-analyzer.ts +0 -557
  9. package/src/analyzers/geo-analyzer.test.ts +0 -310
  10. package/src/analyzers/geo-analyzer.ts +0 -814
  11. package/src/analyzers/image-optimization-analyzer.test.ts +0 -145
  12. package/src/analyzers/image-optimization-analyzer.ts +0 -348
  13. package/src/analyzers/index.ts +0 -233
  14. package/src/analyzers/internal-linking-analyzer.test.ts +0 -141
  15. package/src/analyzers/internal-linking-analyzer.ts +0 -419
  16. package/src/analyzers/mobile-seo-analyzer.test.ts +0 -140
  17. package/src/analyzers/mobile-seo-analyzer.ts +0 -455
  18. package/src/analyzers/security-headers-analyzer.test.ts +0 -115
  19. package/src/analyzers/security-headers-analyzer.ts +0 -318
  20. package/src/analyzers/structured-data-analyzer.test.ts +0 -210
  21. package/src/analyzers/structured-data-analyzer.ts +0 -590
  22. package/src/audit/ai/analyzer.ts +0 -347
  23. package/src/audit/ai/index.ts +0 -29
  24. package/src/audit/ai/prompts/content-analysis.ts +0 -271
  25. package/src/audit/ai/types.ts +0 -179
  26. package/src/audit/checks/additional-checks.ts +0 -439
  27. package/src/audit/checks/agent-experience.ts +0 -108
  28. package/src/audit/checks/ai-citation-worthiness.ts +0 -399
  29. package/src/audit/checks/ai-content-structure.ts +0 -325
  30. package/src/audit/checks/ai-readiness.ts +0 -406
  31. package/src/audit/checks/anchor-text.ts +0 -179
  32. package/src/audit/checks/answer-conciseness.ts +0 -322
  33. package/src/audit/checks/asset-minification.ts +0 -270
  34. package/src/audit/checks/bing-optimization.ts +0 -206
  35. package/src/audit/checks/brand-mention-optimization.ts +0 -349
  36. package/src/audit/checks/caching-headers.ts +0 -305
  37. package/src/audit/checks/canonical-advanced.ts +0 -150
  38. package/src/audit/checks/canonical-domain.ts +0 -196
  39. package/src/audit/checks/citation-quality.ts +0 -358
  40. package/src/audit/checks/client-rendering.ts +0 -542
  41. package/src/audit/checks/color-contrast.ts +0 -342
  42. package/src/audit/checks/content-freshness.ts +0 -170
  43. package/src/audit/checks/content-science.ts +0 -589
  44. package/src/audit/checks/conversion-elements.ts +0 -526
  45. package/src/audit/checks/crawlability.ts +0 -220
  46. package/src/audit/checks/directory-listing.ts +0 -172
  47. package/src/audit/checks/dom-analysis.ts +0 -191
  48. package/src/audit/checks/dom-size.ts +0 -246
  49. package/src/audit/checks/duplicate-content.ts +0 -194
  50. package/src/audit/checks/eeat-signals.ts +0 -990
  51. package/src/audit/checks/entity-seo.ts +0 -396
  52. package/src/audit/checks/featured-snippet.ts +0 -473
  53. package/src/audit/checks/freshness-signals.ts +0 -443
  54. package/src/audit/checks/funnel-intent.ts +0 -463
  55. package/src/audit/checks/hreflang.ts +0 -174
  56. package/src/audit/checks/html-compliance.ts +0 -302
  57. package/src/audit/checks/image-dimensions.ts +0 -167
  58. package/src/audit/checks/images.ts +0 -160
  59. package/src/audit/checks/indexnow.ts +0 -275
  60. package/src/audit/checks/interactive-tools.ts +0 -475
  61. package/src/audit/checks/internal-link-graph.ts +0 -436
  62. package/src/audit/checks/keyword-analysis.ts +0 -239
  63. package/src/audit/checks/keyword-cannibalization.ts +0 -385
  64. package/src/audit/checks/keyword-placement.ts +0 -471
  65. package/src/audit/checks/links.ts +0 -273
  66. package/src/audit/checks/llms-txt.ts +0 -224
  67. package/src/audit/checks/local-seo.ts +0 -296
  68. package/src/audit/checks/mobile.ts +0 -167
  69. package/src/audit/checks/modern-images.ts +0 -226
  70. package/src/audit/checks/navboost-signals.ts +0 -395
  71. package/src/audit/checks/on-page.ts +0 -209
  72. package/src/audit/checks/page-resources.ts +0 -285
  73. package/src/audit/checks/pagination.ts +0 -180
  74. package/src/audit/checks/performance.ts +0 -153
  75. package/src/audit/checks/platform-presence.ts +0 -580
  76. package/src/audit/checks/rag-chunk-readiness.test.ts +0 -159
  77. package/src/audit/checks/rag-chunk-readiness.ts +0 -163
  78. package/src/audit/checks/redirect-analysis.ts +0 -153
  79. package/src/audit/checks/redirect-chain.ts +0 -389
  80. package/src/audit/checks/resource-hints.ts +0 -420
  81. package/src/audit/checks/responsive-css.ts +0 -247
  82. package/src/audit/checks/responsive-images.ts +0 -396
  83. package/src/audit/checks/review-ecosystem.ts +0 -415
  84. package/src/audit/checks/robots-validation.ts +0 -373
  85. package/src/audit/checks/security-headers.ts +0 -188
  86. package/src/audit/checks/security.ts +0 -144
  87. package/src/audit/checks/serp-preview.ts +0 -251
  88. package/src/audit/checks/site-maturity.ts +0 -453
  89. package/src/audit/checks/social-meta.test.ts +0 -275
  90. package/src/audit/checks/social-meta.ts +0 -134
  91. package/src/audit/checks/soft-404.ts +0 -151
  92. package/src/audit/checks/structured-data.ts +0 -238
  93. package/src/audit/checks/tech-detection.ts +0 -496
  94. package/src/audit/checks/topical-clusters.ts +0 -435
  95. package/src/audit/checks/tracker-bloat.ts +0 -462
  96. package/src/audit/checks/tracking-verification.test.ts +0 -371
  97. package/src/audit/checks/tracking-verification.ts +0 -636
  98. package/src/audit/checks/url-safety.ts +0 -682
  99. package/src/audit/deno-entry.ts +0 -66
  100. package/src/audit/discovery/index.ts +0 -15
  101. package/src/audit/discovery/link-crawler.ts +0 -233
  102. package/src/audit/discovery/repo-routes.ts +0 -347
  103. package/src/audit/engine.ts +0 -633
  104. package/src/audit/fixes/index.ts +0 -209
  105. package/src/audit/fixes/social-meta-fixes.test.ts +0 -329
  106. package/src/audit/fixes/social-meta-fixes.ts +0 -463
  107. package/src/audit/index.ts +0 -74
  108. package/src/audit/runner.test.ts +0 -299
  109. package/src/audit/runner.ts +0 -130
  110. package/src/audit/types.ts +0 -1987
  111. package/src/content/featured-snippet.ts +0 -367
  112. package/src/content/generator.test.ts +0 -534
  113. package/src/content/generator.ts +0 -501
  114. package/src/content/headline.ts +0 -317
  115. package/src/content/index.ts +0 -62
  116. package/src/content/intent.ts +0 -258
  117. package/src/content/keyword-density.ts +0 -349
  118. package/src/content/readability.ts +0 -262
  119. package/src/executor.ts +0 -336
  120. package/src/fixer/framework-fixes.test.ts +0 -489
  121. package/src/fixer/framework-fixes.ts +0 -5287
  122. package/src/fixer/index.ts +0 -6
  123. package/src/fixer/schemas.ts +0 -971
  124. package/src/fixer.ts +0 -642
  125. package/src/frameworks/detector.test.ts +0 -248
  126. package/src/frameworks/detector.ts +0 -899
  127. package/src/frameworks/index.ts +0 -68
  128. package/src/frameworks/recipes/angular.yaml +0 -171
  129. package/src/frameworks/recipes/astro.yaml +0 -206
  130. package/src/frameworks/recipes/django.yaml +0 -180
  131. package/src/frameworks/recipes/laravel.yaml +0 -137
  132. package/src/frameworks/recipes/nextjs.yaml +0 -268
  133. package/src/frameworks/recipes/nuxt.yaml +0 -175
  134. package/src/frameworks/recipes/rails.yaml +0 -188
  135. package/src/frameworks/recipes/react.yaml +0 -202
  136. package/src/frameworks/recipes/sveltekit.yaml +0 -154
  137. package/src/frameworks/recipes/vue.yaml +0 -137
  138. package/src/frameworks/recipes/wordpress.yaml +0 -209
  139. package/src/frameworks/suggestion-engine.ts +0 -357
  140. package/src/geo/geo-content.test.ts +0 -305
  141. package/src/geo/geo-content.ts +0 -266
  142. package/src/geo/geo-history.test.ts +0 -473
  143. package/src/geo/geo-history.ts +0 -433
  144. package/src/geo/geo-tracker.test.ts +0 -359
  145. package/src/geo/geo-tracker.ts +0 -411
  146. package/src/geo/index.ts +0 -11
  147. package/src/geo/llm-citation-checker.ts +0 -188
  148. package/src/git/commit-helper.test.ts +0 -261
  149. package/src/git/commit-helper.ts +0 -329
  150. package/src/git/index.ts +0 -12
  151. package/src/git/pr-helper.test.ts +0 -284
  152. package/src/git/pr-helper.ts +0 -307
  153. package/src/index.ts +0 -76
  154. package/src/keywords/ai-keyword-engine.ts +0 -1062
  155. package/src/keywords/ai-summarizer.ts +0 -387
  156. package/src/keywords/ci-mode.ts +0 -555
  157. package/src/keywords/engine.ts +0 -359
  158. package/src/keywords/index.ts +0 -151
  159. package/src/keywords/llm-judge.ts +0 -357
  160. package/src/keywords/nlp-analysis.ts +0 -706
  161. package/src/keywords/prioritizer.ts +0 -295
  162. package/src/keywords/site-crawler.ts +0 -342
  163. package/src/keywords/sources/autocomplete.ts +0 -139
  164. package/src/keywords/sources/competitive-search.ts +0 -450
  165. package/src/keywords/sources/competitor-analysis.ts +0 -374
  166. package/src/keywords/sources/dataforseo.ts +0 -206
  167. package/src/keywords/sources/free-sources.ts +0 -294
  168. package/src/keywords/sources/gsc.ts +0 -123
  169. package/src/keywords/topic-grouping.ts +0 -327
  170. package/src/keywords/types.ts +0 -144
  171. package/src/keywords/wizard.ts +0 -457
  172. package/src/loader.ts +0 -40
  173. package/src/ranking/index.ts +0 -5
  174. package/src/ranking/serp-client.ts +0 -348
  175. package/src/ranking/tracker.ts +0 -380
  176. package/src/ranking/types.ts +0 -123
  177. package/src/reports/index.ts +0 -7
  178. package/src/reports/report-generator.test.ts +0 -293
  179. package/src/reports/report-generator.ts +0 -713
  180. package/src/scheduler/alerts.test.ts +0 -458
  181. package/src/scheduler/alerts.ts +0 -328
  182. package/src/scheduler/index.ts +0 -8
  183. package/src/scheduler/scheduled-audit.test.ts +0 -377
  184. package/src/scheduler/scheduled-audit.ts +0 -149
  185. package/src/test/integration-test.ts +0 -325
  186. package/src/tools/analyzer.ts +0 -373
  187. package/src/tools/crawl.ts +0 -293
  188. package/src/tools/files.ts +0 -301
  189. package/src/tools/h1-fixer.ts +0 -249
  190. package/src/tools/index.ts +0 -67
  191. package/src/tracking/github-action.ts +0 -326
  192. package/src/tracking/google-analytics.ts +0 -265
  193. package/src/tracking/index.ts +0 -45
  194. package/src/tracking/report-generator.ts +0 -386
  195. package/src/tracking/search-console.ts +0 -335
  196. package/src/types.ts +0 -148
  197. package/src/utils/http.ts +0 -302
  198. package/src/wasm-adapter.ts +0 -297
  199. package/src/wasm-entry.ts +0 -14
  200. package/tsconfig.json +0 -17
  201. package/tsup.wasm.config.ts +0 -26
  202. package/vitest.config.ts +0 -15
@@ -1,1062 +0,0 @@
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
- }