@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.
- package/dist/index.d.mts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +373 -1
- package/dist/index.mjs +369 -1
- package/package.json +1 -1
- package/src/audit/checks/color-contrast.ts +1 -1
- package/src/audit/checks/dom-size.ts +1 -1
- package/src/audit/checks/html-compliance.ts +1 -1
- package/src/audit/checks/image-dimensions.ts +1 -1
- package/src/audit/checks/links.ts +70 -0
- package/src/audit/checks/site-maturity.ts +9 -0
- package/src/audit/runner.test.ts +7 -7
- package/src/audit/types.ts +9 -0
- package/src/fixer.ts +220 -0
- package/src/geo/index.ts +1 -0
- package/src/geo/llm-citation-checker.ts +188 -0
- package/src/keywords/sources/free-sources.ts +1 -1
- package/src/scheduler/scheduled-audit.test.ts +6 -6
package/src/audit/runner.test.ts
CHANGED
|
@@ -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'] },
|
package/src/audit/types.ts
CHANGED
|
@@ -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
|
@@ -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'] },
|