@rankcli/agent-runtime 0.0.1 → 0.0.2

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.
@@ -38,7 +38,7 @@ describe('audit-runner', () => {
38
38
  domain: 'example.com',
39
39
  timestamp: '2024-01-01T00:00:00Z',
40
40
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
41
- healthScore: { overall: 65, crawlability: 80, indexability: 70, onPage: 50, content: 70, links: 60, performance: 75, security: 80, aiReadiness: 70, social: 60, localSeo: 50 },
41
+ healthScore: { overall: 65, crawlability: 80, indexability: 70, onPage: 50, content: 70, links: 60, performance: 75, security: 80, aiReadiness: 70, social: 60, localSeo: 50, accessibility: 70 },
42
42
  issues: [
43
43
  { code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title tag', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: ['https://example.com'] },
44
44
  { code: 'OG_IMAGE_MISSING', severity: 'warning', category: 'social', title: 'Missing OG image', description: 'No og:image', impact: 'Social shares will not display a preview image.', howToFix: 'Add og:image meta tag.', affectedUrls: ['https://example.com'] },
@@ -72,7 +72,7 @@ describe('audit-runner', () => {
72
72
  domain: 'example.com',
73
73
  timestamp: '2024-01-01T00:00:00Z',
74
74
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
75
- healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
75
+ healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80, accessibility: 80 },
76
76
  issues: [],
77
77
  pages: [],
78
78
  summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
@@ -101,7 +101,7 @@ describe('audit-runner', () => {
101
101
  domain: 'example.com',
102
102
  timestamp: '2024-01-01T00:00:00Z',
103
103
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
104
- healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
104
+ healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80, accessibility: 80 },
105
105
  issues: [{ code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: [] }],
106
106
  pages: [],
107
107
  summary: { errors: 1, warnings: 0, notices: 0, passed: 84 },
@@ -136,7 +136,7 @@ describe('audit-runner', () => {
136
136
  domain: 'example.com',
137
137
  timestamp: '2024-01-01T00:00:00Z',
138
138
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
139
- healthScore: { overall: 82, crawlability: 80, indexability: 80, onPage: 85, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
139
+ healthScore: { overall: 82, crawlability: 80, indexability: 80, onPage: 85, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80, accessibility: 80 },
140
140
  issues: [],
141
141
  pages: [],
142
142
  summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
@@ -180,7 +180,7 @@ describe('audit-runner', () => {
180
180
  domain: 'example.com',
181
181
  timestamp: '2024-01-01T00:00:00Z',
182
182
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
183
- healthScore: { overall: 95, crawlability: 95, indexability: 95, onPage: 95, content: 95, links: 95, performance: 95, security: 95, aiReadiness: 95, social: 95, localSeo: 95 },
183
+ healthScore: { overall: 95, crawlability: 95, indexability: 95, onPage: 95, content: 95, links: 95, performance: 95, security: 95, aiReadiness: 95, social: 95, localSeo: 95, accessibility: 95 },
184
184
  issues: [],
185
185
  pages: [],
186
186
  summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
@@ -207,7 +207,7 @@ describe('audit-runner', () => {
207
207
  domain: 'example.com',
208
208
  timestamp: '2024-01-01T00:00:00Z',
209
209
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
210
- healthScore: { overall: 82, crawlability: 80, indexability: 80, onPage: 85, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
210
+ healthScore: { overall: 82, crawlability: 80, indexability: 80, onPage: 85, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80, accessibility: 80 },
211
211
  issues: [],
212
212
  pages: [],
213
213
  summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
@@ -246,7 +246,7 @@ describe('audit-runner', () => {
246
246
  domain: 'myapp.com',
247
247
  timestamp: '2024-01-01T00:00:00Z',
248
248
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
249
- healthScore: { overall: 45, crawlability: 60, indexability: 50, onPage: 30, content: 40, links: 50, performance: 60, security: 70, aiReadiness: 50, social: 40, localSeo: 45 },
249
+ healthScore: { overall: 45, crawlability: 60, indexability: 50, onPage: 30, content: 40, links: 50, performance: 60, security: 70, aiReadiness: 50, social: 40, localSeo: 45, accessibility: 45 },
250
250
  issues: [
251
251
  { code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: ['https://myapp.com'] },
252
252
  { code: 'META_DESC_MISSING', severity: 'error', category: 'on-page', title: 'Missing description', description: 'No meta desc', impact: 'Search results will show auto-generated snippets.', howToFix: 'Add a meta description.', affectedUrls: ['https://myapp.com'] },
@@ -385,6 +385,15 @@ export const ISSUE_DEFINITIONS: Record<string, IssueDefinition> = {
385
385
  impact: 'Poor user experience and potential trust issues.',
386
386
  howToFix: 'Update or remove the broken external link.',
387
387
  },
388
+ JS_ONLY_NAVIGATION: {
389
+ code: 'JS_ONLY_NAVIGATION',
390
+ severity: 'warning',
391
+ category: 'links',
392
+ title: 'JavaScript-only navigation detected',
393
+ description: 'Navigation elements use onClick handlers without proper <a href> links.',
394
+ impact: 'Search engine crawlers cannot follow JavaScript-only navigation. These links are invisible to crawlers, preventing page discovery and indexing.',
395
+ howToFix: 'Replace onClick navigation with proper <a href> or <Link> components. In React, use react-router Link instead of navigate() in onClick handlers. Ensure all navigation renders as real anchor tags with href attributes.',
396
+ },
388
397
  TOO_MANY_LINKS: {
389
398
  code: 'TOO_MANY_LINKS',
390
399
  severity: 'notice',
package/src/fixer.ts CHANGED
@@ -71,36 +71,87 @@ async function generateFixForIssue(
71
71
  const siteName = new URL(fullUrl).hostname.replace('www.', '');
72
72
 
73
73
  switch (issue.code) {
74
+ // Title issues
74
75
  case 'MISSING_TITLE':
76
+ case 'TITLE_KEYWORD_MISMATCH':
77
+ case 'TITLE_H1_KEYWORD_MISMATCH':
78
+ case 'OUTDATED_YEAR_IN_TITLE':
75
79
  return generateTitleFix(context, siteName);
76
80
 
81
+ // Meta description issues
77
82
  case 'MISSING_META_DESC':
78
83
  return generateMetaDescFix(context, siteName);
79
84
 
85
+ // Canonical issues
80
86
  case 'MISSING_CANONICAL':
87
+ case 'CANONICAL_NO_HTTPS_REDIRECT':
81
88
  return generateCanonicalFix(context, fullUrl);
82
89
 
90
+ // Viewport issues
83
91
  case 'MISSING_VIEWPORT':
92
+ case 'HTML_NO_VIEWPORT':
93
+ case 'RESPONSIVE_NO_VIEWPORT':
84
94
  return generateViewportFix(context);
85
95
 
96
+ // Open Graph issues
86
97
  case 'MISSING_OG_TAGS':
87
98
  return generateOGFix(context, siteName, fullUrl);
88
99
 
100
+ // Twitter Card issues
89
101
  case 'MISSING_TWITTER_CARD':
90
102
  return generateTwitterFix(context, siteName);
91
103
 
104
+ // Schema/structured data issues
92
105
  case 'MISSING_SCHEMA':
106
+ case 'SCHEMA_ORG_MISSING':
107
+ case 'NO_ORGANIZATION_SCHEMA':
108
+ case 'NO_ENTITY_SCHEMA':
109
+ case 'FAQ_SCHEMA_MISSING':
93
110
  return generateSchemaFix(context, siteName, fullUrl);
94
111
 
112
+ // Robots.txt issues
95
113
  case 'MISSING_ROBOTS':
114
+ case 'ROBOTS_TXT_WARNINGS':
115
+ case 'ROBOTS_TXT_INVALID_SYNTAX':
96
116
  return generateRobotsFix(context, fullUrl);
97
117
 
118
+ // Sitemap issues
98
119
  case 'MISSING_SITEMAP':
120
+ case 'BING_SITEMAP_MISSING':
99
121
  return generateSitemapFix(context, fullUrl);
100
122
 
123
+ // H1 issues
101
124
  case 'MISSING_H1':
125
+ case 'NO_VISIBLE_HEADLINE':
126
+ case 'H1_MISSING_KEYWORD':
102
127
  return await generateH1Fix({ cwd });
103
128
 
129
+ // SPA-specific: add meta management library recommendation
130
+ case 'SPA_NO_META_MANAGEMENT':
131
+ return generateSPAMetaFix(context, framework);
132
+
133
+ // Preconnect issues
134
+ case 'GOOGLE_FONTS_NO_PRECONNECT':
135
+ case 'MISSING_PRECONNECT':
136
+ return generatePreconnectFix(context);
137
+
138
+ // Favicon/icons
139
+ case 'HTML_NO_FAVICON':
140
+ case 'HTML_NO_APPLE_TOUCH_ICON':
141
+ return generateFaviconFix(context);
142
+
143
+ // Charset/lang
144
+ case 'HTML_NO_CHARSET':
145
+ case 'HTML_NOT_UTF8':
146
+ return generateCharsetFix(context);
147
+
148
+ case 'HTML_NO_LANG':
149
+ return generateLangFix(context);
150
+
151
+ // AI/LLMs.txt
152
+ case 'AI_NO_LLMS_TXT':
153
+ return generateLlmsTxtFix(context, siteName, fullUrl);
154
+
104
155
  default:
105
156
  return null;
106
157
  }
@@ -358,6 +409,175 @@ function generateSitemapFix(context: FixContext, url: string): GeneratedFix {
358
409
  };
359
410
  }
360
411
 
412
+ function generateSPAMetaFix(context: FixContext, framework: FrameworkInfo): GeneratedFix {
413
+ const { htmlPath } = context;
414
+
415
+ // For React apps, recommend react-helmet-async
416
+ if (framework.name.toLowerCase().includes('react') || framework.name === 'Unknown') {
417
+ return {
418
+ issue: { code: 'SPA_NO_META_MANAGEMENT', message: 'SPA without dynamic meta tag management', severity: 'warning' },
419
+ file: 'src/components/SEOHead.tsx',
420
+ before: null,
421
+ after: `import { Helmet } from 'react-helmet-async';
422
+
423
+ interface SEOHeadProps {
424
+ title?: string;
425
+ description?: string;
426
+ image?: string;
427
+ url?: string;
428
+ }
429
+
430
+ export function SEOHead({
431
+ title = 'Your Site Name',
432
+ description = 'Your site description',
433
+ image = '/og-image.png',
434
+ url = window.location.href,
435
+ }: SEOHeadProps) {
436
+ return (
437
+ <Helmet>
438
+ <title>{title}</title>
439
+ <meta name="description" content={description} />
440
+ <link rel="canonical" href={url} />
441
+
442
+ {/* Open Graph */}
443
+ <meta property="og:title" content={title} />
444
+ <meta property="og:description" content={description} />
445
+ <meta property="og:image" content={image} />
446
+ <meta property="og:url" content={url} />
447
+ <meta property="og:type" content="website" />
448
+
449
+ {/* Twitter */}
450
+ <meta name="twitter:card" content="summary_large_image" />
451
+ <meta name="twitter:title" content={title} />
452
+ <meta name="twitter:description" content={description} />
453
+ <meta name="twitter:image" content={image} />
454
+ </Helmet>
455
+ );
456
+ }`,
457
+ explanation: 'Created SEOHead component using react-helmet-async for dynamic meta tags. Install: npm install react-helmet-async',
458
+ };
459
+ }
460
+
461
+ return {
462
+ issue: { code: 'SPA_NO_META_MANAGEMENT', message: 'SPA without meta management', severity: 'warning' },
463
+ file: htmlPath,
464
+ before: null,
465
+ after: '<!-- Add a meta management library for your framework -->',
466
+ explanation: `Add dynamic meta tag management for ${framework.name}`,
467
+ skipped: true,
468
+ skipReason: `Framework-specific solution needed for ${framework.name}`,
469
+ };
470
+ }
471
+
472
+ function generatePreconnectFix(context: FixContext): GeneratedFix {
473
+ const { htmlPath, htmlContent } = context;
474
+
475
+ const preconnects = `<!-- Preconnect to external origins -->
476
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
477
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />`;
478
+
479
+ return {
480
+ issue: { code: 'MISSING_PRECONNECT', message: 'Missing preconnect hints', severity: 'info' },
481
+ file: htmlPath,
482
+ before: '<head>',
483
+ after: `<head>\n ${preconnects}`,
484
+ explanation: 'Added preconnect hints to speed up loading of external resources',
485
+ };
486
+ }
487
+
488
+ function generateFaviconFix(context: FixContext): GeneratedFix {
489
+ const { htmlPath } = context;
490
+
491
+ const faviconTags = `<!-- Favicons -->
492
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
493
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
494
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />`;
495
+
496
+ return {
497
+ issue: { code: 'HTML_NO_FAVICON', message: 'Missing favicon', severity: 'info' },
498
+ file: htmlPath,
499
+ before: '<head>',
500
+ after: `<head>\n ${faviconTags}`,
501
+ explanation: 'Added favicon links. Create favicon files in public/ directory.',
502
+ };
503
+ }
504
+
505
+ function generateCharsetFix(context: FixContext): GeneratedFix {
506
+ const { htmlPath, htmlContent } = context;
507
+
508
+ if (htmlContent.includes('charset')) {
509
+ return {
510
+ issue: { code: 'HTML_NO_CHARSET', message: 'Missing charset', severity: 'warning' },
511
+ file: htmlPath,
512
+ before: null,
513
+ after: '<meta charset="UTF-8" />',
514
+ skipped: true,
515
+ skipReason: 'Charset already defined',
516
+ explanation: 'Charset is already present',
517
+ };
518
+ }
519
+
520
+ return {
521
+ issue: { code: 'HTML_NO_CHARSET', message: 'Missing charset declaration', severity: 'warning' },
522
+ file: htmlPath,
523
+ before: '<head>',
524
+ after: '<head>\n <meta charset="UTF-8" />',
525
+ explanation: 'Added UTF-8 charset declaration as first element in head',
526
+ };
527
+ }
528
+
529
+ function generateLangFix(context: FixContext): GeneratedFix {
530
+ const { htmlPath, htmlContent } = context;
531
+
532
+ if (htmlContent.includes('<html') && !htmlContent.includes('lang=')) {
533
+ return {
534
+ issue: { code: 'HTML_NO_LANG', message: 'Missing lang attribute', severity: 'warning' },
535
+ file: htmlPath,
536
+ before: '<html>',
537
+ after: '<html lang="en">',
538
+ explanation: 'Added lang attribute for accessibility and SEO',
539
+ };
540
+ }
541
+
542
+ return {
543
+ issue: { code: 'HTML_NO_LANG', message: 'Missing lang attribute', severity: 'warning' },
544
+ file: htmlPath,
545
+ before: '<html',
546
+ after: '<html lang="en"',
547
+ explanation: 'Added lang attribute for accessibility and SEO',
548
+ };
549
+ }
550
+
551
+ function generateLlmsTxtFix(context: FixContext, siteName: string, url: string): GeneratedFix {
552
+ const llmsTxt = `# ${siteName}
553
+ > ${siteName} - A brief description of your product/service
554
+
555
+ ## About
556
+ ${siteName} is... [Add your description here]
557
+
558
+ ## Features
559
+ - Feature 1
560
+ - Feature 2
561
+ - Feature 3
562
+
563
+ ## Links
564
+ - Homepage: ${url}
565
+ - Documentation: ${url}/docs
566
+ - API: ${url}/api
567
+
568
+ ## Contact
569
+ - Email: hello@${new URL(url).hostname}
570
+ `;
571
+
572
+ return {
573
+ issue: { code: 'AI_NO_LLMS_TXT', message: 'No llms.txt file for AI crawlers', severity: 'info' },
574
+ file: 'public/llms.txt',
575
+ before: null,
576
+ after: llmsTxt,
577
+ explanation: 'Created llms.txt to help AI systems understand your site. Customize the content.',
578
+ };
579
+ }
580
+
361
581
  // Apply fixes to the codebase
362
582
  export async function applyFixes(
363
583
  fixes: GeneratedFix[],
package/src/geo/index.ts CHANGED
@@ -8,3 +8,4 @@
8
8
  export * from './geo-tracker.js';
9
9
  export * from './geo-history.js';
10
10
  export * from './geo-content.js';
11
+ export * from './llm-citation-checker.js';
@@ -0,0 +1,188 @@
1
+ /**
2
+ * LLM Citation Checker
3
+ *
4
+ * Simplified interface for checking if a brand is mentioned/cited
5
+ * by major LLM providers when asked recommendation queries.
6
+ */
7
+
8
+ import {
9
+ trackLLMVisibility,
10
+ parseGEOResponse,
11
+ type BrandConfig,
12
+ type GEOResult,
13
+ type LLMProvider,
14
+ type TrackingOptions,
15
+ } from './geo-tracker.js';
16
+
17
+ export interface LLMCitationResult {
18
+ provider: LLMProvider;
19
+ providerName: string;
20
+ mentioned: boolean;
21
+ position: number | null;
22
+ sentiment: 'positive' | 'neutral' | 'negative' | null;
23
+ context: string | null;
24
+ score: number;
25
+ error?: string;
26
+ }
27
+
28
+ export interface CitationCheckOptions extends TrackingOptions {
29
+ providers?: LLMProvider[];
30
+ queries?: string[];
31
+ }
32
+
33
+ const PROVIDER_NAMES: Record<LLMProvider, string> = {
34
+ openai: 'ChatGPT',
35
+ anthropic: 'Claude',
36
+ google: 'Gemini',
37
+ perplexity: 'Perplexity',
38
+ };
39
+
40
+ const DEFAULT_PROVIDERS: LLMProvider[] = ['openai', 'anthropic', 'google', 'perplexity'];
41
+
42
+ /**
43
+ * Generate recommendation queries for a brand
44
+ */
45
+ export function generateRecommendationQueries(brand: string, industry?: string): string[] {
46
+ const queries = [
47
+ `What are the best ${brand.toLowerCase()} alternatives?`,
48
+ `Can you recommend tools similar to ${brand}?`,
49
+ `What do you think about ${brand}?`,
50
+ ];
51
+
52
+ if (industry) {
53
+ queries.push(`What are the best ${industry} tools?`);
54
+ queries.push(`Recommend a good ${industry} solution`);
55
+ }
56
+
57
+ return queries;
58
+ }
59
+
60
+ /**
61
+ * Check if a brand is cited/mentioned across LLM providers
62
+ */
63
+ export async function checkLLMCitations(
64
+ brand: string,
65
+ domain: string,
66
+ options: CitationCheckOptions = {}
67
+ ): Promise<LLMCitationResult[]> {
68
+ const {
69
+ providers = DEFAULT_PROVIDERS,
70
+ queries = generateRecommendationQueries(brand),
71
+ ...trackingOptions
72
+ } = options;
73
+
74
+ const brandConfig: BrandConfig = {
75
+ brandName: brand,
76
+ domains: [domain],
77
+ alternativeNames: [brand.toLowerCase(), brand.toUpperCase()],
78
+ };
79
+
80
+ // Use the first query for the check
81
+ const query = queries[0];
82
+
83
+ const results = await trackLLMVisibility(
84
+ {
85
+ keyword: query,
86
+ brand: brandConfig,
87
+ providers,
88
+ },
89
+ trackingOptions
90
+ );
91
+
92
+ return results.map((result) => ({
93
+ provider: result.provider,
94
+ providerName: PROVIDER_NAMES[result.provider],
95
+ mentioned: result.mentioned,
96
+ position: result.position,
97
+ sentiment: result.sentiment,
98
+ context: result.contextSnippet || null,
99
+ score: result.score,
100
+ error: result.error,
101
+ }));
102
+ }
103
+
104
+ /**
105
+ * Calculate overall AI visibility score from citation results
106
+ */
107
+ export function calculateAIVisibilityScore(results: LLMCitationResult[]): number {
108
+ if (results.length === 0) return 0;
109
+
110
+ const mentionedCount = results.filter((r) => r.mentioned && !r.error).length;
111
+ const validResults = results.filter((r) => !r.error);
112
+
113
+ if (validResults.length === 0) return 0;
114
+
115
+ // Base score: 25 points per provider mention (out of 100 for 4 providers)
116
+ let score = (mentionedCount / validResults.length) * 100;
117
+
118
+ // Bonus for good positions (top 3)
119
+ const topPositions = results.filter(
120
+ (r) => r.mentioned && r.position !== null && r.position <= 3
121
+ );
122
+ if (topPositions.length > 0) {
123
+ score = Math.min(100, score + 10);
124
+ }
125
+
126
+ // Bonus for positive sentiment
127
+ const positiveResults = results.filter((r) => r.sentiment === 'positive');
128
+ if (positiveResults.length > mentionedCount / 2) {
129
+ score = Math.min(100, score + 5);
130
+ }
131
+
132
+ return Math.round(score);
133
+ }
134
+
135
+ /**
136
+ * Get a summary of AI visibility across providers
137
+ */
138
+ export function getAIVisibilitySummary(results: LLMCitationResult[]): {
139
+ score: number;
140
+ mentionedIn: string[];
141
+ notMentionedIn: string[];
142
+ bestPosition: number | null;
143
+ overallSentiment: 'positive' | 'neutral' | 'negative' | 'mixed' | null;
144
+ } {
145
+ const score = calculateAIVisibilityScore(results);
146
+
147
+ const mentionedIn = results
148
+ .filter((r) => r.mentioned)
149
+ .map((r) => r.providerName);
150
+
151
+ const notMentionedIn = results
152
+ .filter((r) => !r.mentioned && !r.error)
153
+ .map((r) => r.providerName);
154
+
155
+ const positions = results
156
+ .filter((r) => r.position !== null)
157
+ .map((r) => r.position as number);
158
+ const bestPosition = positions.length > 0 ? Math.min(...positions) : null;
159
+
160
+ // Calculate overall sentiment
161
+ const sentiments = results
162
+ .filter((r) => r.sentiment !== null)
163
+ .map((r) => r.sentiment);
164
+
165
+ let overallSentiment: 'positive' | 'neutral' | 'negative' | 'mixed' | null = null;
166
+ if (sentiments.length > 0) {
167
+ const positiveCount = sentiments.filter((s) => s === 'positive').length;
168
+ const negativeCount = sentiments.filter((s) => s === 'negative').length;
169
+
170
+ if (positiveCount > 0 && negativeCount > 0) {
171
+ overallSentiment = 'mixed';
172
+ } else if (positiveCount > negativeCount) {
173
+ overallSentiment = 'positive';
174
+ } else if (negativeCount > positiveCount) {
175
+ overallSentiment = 'negative';
176
+ } else {
177
+ overallSentiment = 'neutral';
178
+ }
179
+ }
180
+
181
+ return {
182
+ score,
183
+ mentionedIn,
184
+ notMentionedIn,
185
+ bestPosition,
186
+ overallSentiment,
187
+ };
188
+ }
@@ -73,7 +73,7 @@ export async function getRelatedSearches(query: string): Promise<string[]> {
73
73
  // 3. Wikipedia Topics for Long-Tail Ideas
74
74
  export async function getWikipediaTopics(query: string): Promise<string[]> {
75
75
  try {
76
- const response = await httpGet<string>('https://en.wikipedia.org/w/api.php', {
76
+ const response = await httpGet<[string, string[], string[], string[]]>('https://en.wikipedia.org/w/api.php', {
77
77
  params: {
78
78
  action: 'opensearch',
79
79
  search: query,
@@ -136,7 +136,7 @@ describe('scheduled-audit', () => {
136
136
  domain: 'example.com',
137
137
  timestamp: '2024-01-15T10:00:00Z',
138
138
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
139
- healthScore: { overall: 75, crawlability: 80, indexability: 75, onPage: 70, content: 80, links: 75, performance: 70, security: 80, aiReadiness: 75, social: 70, localSeo: 70 },
139
+ healthScore: { overall: 75, crawlability: 80, indexability: 75, onPage: 70, content: 80, links: 75, performance: 70, security: 80, aiReadiness: 75, social: 70, localSeo: 70, accessibility: 70 },
140
140
  issues: [],
141
141
  pages: [],
142
142
  summary: { errors: 0, warnings: 2, notices: 1, passed: 82 },
@@ -170,7 +170,7 @@ describe('scheduled-audit', () => {
170
170
  domain: 'example.com',
171
171
  timestamp: '2024-01-15T10:00:00Z',
172
172
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
173
- healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
173
+ healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80, accessibility: 80 },
174
174
  issues: [],
175
175
  pages: [],
176
176
  summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
@@ -204,7 +204,7 @@ describe('scheduled-audit', () => {
204
204
  domain: 'example.com',
205
205
  timestamp: '2024-01-15T10:00:00Z',
206
206
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
207
- healthScore: { overall: 65, crawlability: 80, indexability: 70, onPage: 50, content: 70, links: 60, performance: 75, security: 80, aiReadiness: 65, social: 60, localSeo: 60 },
207
+ healthScore: { overall: 65, crawlability: 80, indexability: 70, onPage: 50, content: 70, links: 60, performance: 75, security: 80, aiReadiness: 65, social: 60, localSeo: 60, accessibility: 60 },
208
208
  issues: [{ code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: [] }],
209
209
  pages: [],
210
210
  summary: { errors: 1, warnings: 0, notices: 0, passed: 84 },
@@ -244,7 +244,7 @@ describe('scheduled-audit', () => {
244
244
  domain: 'example.com',
245
245
  timestamp: '2024-01-15T10:00:00Z',
246
246
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
247
- healthScore: { overall: 65, crawlability: 80, indexability: 70, onPage: 50, content: 70, links: 60, performance: 75, security: 80, aiReadiness: 65, social: 60, localSeo: 60 },
247
+ healthScore: { overall: 65, crawlability: 80, indexability: 70, onPage: 50, content: 70, links: 60, performance: 75, security: 80, aiReadiness: 65, social: 60, localSeo: 60, accessibility: 60 },
248
248
  issues: [{ code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: [] }],
249
249
  pages: [],
250
250
  summary: { errors: 1, warnings: 0, notices: 0, passed: 84 },
@@ -293,7 +293,7 @@ describe('scheduled-audit', () => {
293
293
  domain: 'example.com',
294
294
  timestamp: '2024-01-15T10:00:00Z',
295
295
  crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
296
- healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
296
+ healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80, accessibility: 80 },
297
297
  issues: [],
298
298
  pages: [],
299
299
  summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
@@ -328,7 +328,7 @@ describe('scheduled-audit', () => {
328
328
  domain: 'myapp.com',
329
329
  timestamp: '2024-01-15T10:00:00Z',
330
330
  crawlStats: { totalUrls: 5, crawledUrls: 5, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
331
- healthScore: { overall: 58, crawlability: 70, indexability: 60, onPage: 45, content: 55, links: 60, performance: 65, security: 75, aiReadiness: 55, social: 50, localSeo: 50 },
331
+ healthScore: { overall: 58, crawlability: 70, indexability: 60, onPage: 45, content: 55, links: 60, performance: 65, security: 75, aiReadiness: 55, social: 50, localSeo: 50, accessibility: 50 },
332
332
  issues: [
333
333
  { code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: ['https://myapp.com/about'] },
334
334
  { code: 'META_DESC_MISSING', severity: 'error', category: 'on-page', title: 'Missing meta description', description: 'No meta', impact: 'Search results will show auto-generated snippets.', howToFix: 'Add a meta description.', affectedUrls: ['https://myapp.com/about'] },