@rankcli/agent-runtime 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +242 -0
  2. package/dist/analyzer-2CSWIQGD.mjs +6 -0
  3. package/dist/chunk-YNZYHEYM.mjs +774 -0
  4. package/dist/index.d.mts +4012 -0
  5. package/dist/index.d.ts +4012 -0
  6. package/dist/index.js +29672 -0
  7. package/dist/index.mjs +28602 -0
  8. package/package.json +53 -0
  9. package/scripts/build-deno.ts +134 -0
  10. package/src/audit/ai/analyzer.ts +347 -0
  11. package/src/audit/ai/index.ts +29 -0
  12. package/src/audit/ai/prompts/content-analysis.ts +271 -0
  13. package/src/audit/ai/types.ts +179 -0
  14. package/src/audit/checks/additional-checks.ts +439 -0
  15. package/src/audit/checks/ai-citation-worthiness.ts +399 -0
  16. package/src/audit/checks/ai-content-structure.ts +325 -0
  17. package/src/audit/checks/ai-readiness.ts +339 -0
  18. package/src/audit/checks/anchor-text.ts +179 -0
  19. package/src/audit/checks/answer-conciseness.ts +322 -0
  20. package/src/audit/checks/asset-minification.ts +270 -0
  21. package/src/audit/checks/bing-optimization.ts +206 -0
  22. package/src/audit/checks/brand-mention-optimization.ts +349 -0
  23. package/src/audit/checks/caching-headers.ts +305 -0
  24. package/src/audit/checks/canonical-advanced.ts +150 -0
  25. package/src/audit/checks/canonical-domain.ts +196 -0
  26. package/src/audit/checks/citation-quality.ts +358 -0
  27. package/src/audit/checks/client-rendering.ts +542 -0
  28. package/src/audit/checks/color-contrast.ts +342 -0
  29. package/src/audit/checks/content-freshness.ts +170 -0
  30. package/src/audit/checks/content-science.ts +589 -0
  31. package/src/audit/checks/conversion-elements.ts +526 -0
  32. package/src/audit/checks/crawlability.ts +220 -0
  33. package/src/audit/checks/directory-listing.ts +172 -0
  34. package/src/audit/checks/dom-analysis.ts +191 -0
  35. package/src/audit/checks/dom-size.ts +246 -0
  36. package/src/audit/checks/duplicate-content.ts +194 -0
  37. package/src/audit/checks/eeat-signals.ts +990 -0
  38. package/src/audit/checks/entity-seo.ts +396 -0
  39. package/src/audit/checks/featured-snippet.ts +473 -0
  40. package/src/audit/checks/freshness-signals.ts +443 -0
  41. package/src/audit/checks/funnel-intent.ts +463 -0
  42. package/src/audit/checks/hreflang.ts +174 -0
  43. package/src/audit/checks/html-compliance.ts +302 -0
  44. package/src/audit/checks/image-dimensions.ts +167 -0
  45. package/src/audit/checks/images.ts +160 -0
  46. package/src/audit/checks/indexnow.ts +275 -0
  47. package/src/audit/checks/interactive-tools.ts +475 -0
  48. package/src/audit/checks/internal-link-graph.ts +436 -0
  49. package/src/audit/checks/keyword-analysis.ts +239 -0
  50. package/src/audit/checks/keyword-cannibalization.ts +385 -0
  51. package/src/audit/checks/keyword-placement.ts +471 -0
  52. package/src/audit/checks/links.ts +203 -0
  53. package/src/audit/checks/llms-txt.ts +224 -0
  54. package/src/audit/checks/local-seo.ts +296 -0
  55. package/src/audit/checks/mobile.ts +167 -0
  56. package/src/audit/checks/modern-images.ts +226 -0
  57. package/src/audit/checks/navboost-signals.ts +395 -0
  58. package/src/audit/checks/on-page.ts +209 -0
  59. package/src/audit/checks/page-resources.ts +285 -0
  60. package/src/audit/checks/pagination.ts +180 -0
  61. package/src/audit/checks/performance.ts +153 -0
  62. package/src/audit/checks/platform-presence.ts +580 -0
  63. package/src/audit/checks/redirect-analysis.ts +153 -0
  64. package/src/audit/checks/redirect-chain.ts +389 -0
  65. package/src/audit/checks/resource-hints.ts +420 -0
  66. package/src/audit/checks/responsive-css.ts +247 -0
  67. package/src/audit/checks/responsive-images.ts +396 -0
  68. package/src/audit/checks/review-ecosystem.ts +415 -0
  69. package/src/audit/checks/robots-validation.ts +373 -0
  70. package/src/audit/checks/security-headers.ts +172 -0
  71. package/src/audit/checks/security.ts +144 -0
  72. package/src/audit/checks/serp-preview.ts +251 -0
  73. package/src/audit/checks/site-maturity.ts +444 -0
  74. package/src/audit/checks/social-meta.test.ts +275 -0
  75. package/src/audit/checks/social-meta.ts +134 -0
  76. package/src/audit/checks/soft-404.ts +151 -0
  77. package/src/audit/checks/structured-data.ts +238 -0
  78. package/src/audit/checks/tech-detection.ts +496 -0
  79. package/src/audit/checks/topical-clusters.ts +435 -0
  80. package/src/audit/checks/tracker-bloat.ts +462 -0
  81. package/src/audit/checks/tracking-verification.test.ts +371 -0
  82. package/src/audit/checks/tracking-verification.ts +636 -0
  83. package/src/audit/checks/url-safety.ts +682 -0
  84. package/src/audit/deno-entry.ts +66 -0
  85. package/src/audit/discovery/index.ts +15 -0
  86. package/src/audit/discovery/link-crawler.ts +232 -0
  87. package/src/audit/discovery/repo-routes.ts +347 -0
  88. package/src/audit/engine.ts +620 -0
  89. package/src/audit/fixes/index.ts +209 -0
  90. package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
  91. package/src/audit/fixes/social-meta-fixes.ts +463 -0
  92. package/src/audit/index.ts +74 -0
  93. package/src/audit/runner.test.ts +299 -0
  94. package/src/audit/runner.ts +130 -0
  95. package/src/audit/types.ts +1953 -0
  96. package/src/content/featured-snippet.ts +367 -0
  97. package/src/content/generator.test.ts +534 -0
  98. package/src/content/generator.ts +501 -0
  99. package/src/content/headline.ts +317 -0
  100. package/src/content/index.ts +62 -0
  101. package/src/content/intent.ts +258 -0
  102. package/src/content/keyword-density.ts +349 -0
  103. package/src/content/readability.ts +262 -0
  104. package/src/executor.ts +336 -0
  105. package/src/fixer.ts +416 -0
  106. package/src/frameworks/detector.test.ts +248 -0
  107. package/src/frameworks/detector.ts +371 -0
  108. package/src/frameworks/index.ts +68 -0
  109. package/src/frameworks/recipes/angular.yaml +171 -0
  110. package/src/frameworks/recipes/astro.yaml +206 -0
  111. package/src/frameworks/recipes/django.yaml +180 -0
  112. package/src/frameworks/recipes/laravel.yaml +137 -0
  113. package/src/frameworks/recipes/nextjs.yaml +268 -0
  114. package/src/frameworks/recipes/nuxt.yaml +175 -0
  115. package/src/frameworks/recipes/rails.yaml +188 -0
  116. package/src/frameworks/recipes/react.yaml +202 -0
  117. package/src/frameworks/recipes/sveltekit.yaml +154 -0
  118. package/src/frameworks/recipes/vue.yaml +137 -0
  119. package/src/frameworks/recipes/wordpress.yaml +209 -0
  120. package/src/frameworks/suggestion-engine.ts +320 -0
  121. package/src/geo/geo-content.test.ts +305 -0
  122. package/src/geo/geo-content.ts +266 -0
  123. package/src/geo/geo-history.test.ts +473 -0
  124. package/src/geo/geo-history.ts +433 -0
  125. package/src/geo/geo-tracker.test.ts +359 -0
  126. package/src/geo/geo-tracker.ts +411 -0
  127. package/src/geo/index.ts +10 -0
  128. package/src/git/commit-helper.test.ts +261 -0
  129. package/src/git/commit-helper.ts +329 -0
  130. package/src/git/index.ts +12 -0
  131. package/src/git/pr-helper.test.ts +284 -0
  132. package/src/git/pr-helper.ts +307 -0
  133. package/src/index.ts +66 -0
  134. package/src/keywords/ai-keyword-engine.ts +1062 -0
  135. package/src/keywords/ai-summarizer.ts +387 -0
  136. package/src/keywords/ci-mode.ts +555 -0
  137. package/src/keywords/engine.ts +359 -0
  138. package/src/keywords/index.ts +151 -0
  139. package/src/keywords/llm-judge.ts +357 -0
  140. package/src/keywords/nlp-analysis.ts +706 -0
  141. package/src/keywords/prioritizer.ts +295 -0
  142. package/src/keywords/site-crawler.ts +342 -0
  143. package/src/keywords/sources/autocomplete.ts +139 -0
  144. package/src/keywords/sources/competitive-search.ts +450 -0
  145. package/src/keywords/sources/competitor-analysis.ts +374 -0
  146. package/src/keywords/sources/dataforseo.ts +206 -0
  147. package/src/keywords/sources/free-sources.ts +294 -0
  148. package/src/keywords/sources/gsc.ts +123 -0
  149. package/src/keywords/topic-grouping.ts +327 -0
  150. package/src/keywords/types.ts +144 -0
  151. package/src/keywords/wizard.ts +457 -0
  152. package/src/loader.ts +40 -0
  153. package/src/reports/index.ts +7 -0
  154. package/src/reports/report-generator.test.ts +293 -0
  155. package/src/reports/report-generator.ts +713 -0
  156. package/src/scheduler/alerts.test.ts +458 -0
  157. package/src/scheduler/alerts.ts +328 -0
  158. package/src/scheduler/index.ts +8 -0
  159. package/src/scheduler/scheduled-audit.test.ts +377 -0
  160. package/src/scheduler/scheduled-audit.ts +149 -0
  161. package/src/test/integration-test.ts +325 -0
  162. package/src/tools/analyzer.ts +373 -0
  163. package/src/tools/crawl.ts +293 -0
  164. package/src/tools/files.ts +301 -0
  165. package/src/tools/h1-fixer.ts +249 -0
  166. package/src/tools/index.ts +67 -0
  167. package/src/tracking/github-action.ts +326 -0
  168. package/src/tracking/google-analytics.ts +265 -0
  169. package/src/tracking/index.ts +45 -0
  170. package/src/tracking/report-generator.ts +386 -0
  171. package/src/tracking/search-console.ts +335 -0
  172. package/src/types.ts +134 -0
  173. package/src/utils/http.ts +302 -0
  174. package/src/wasm-adapter.ts +297 -0
  175. package/src/wasm-entry.ts +14 -0
  176. package/tsconfig.json +17 -0
  177. package/tsup.wasm.config.ts +26 -0
  178. package/vitest.config.ts +15 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Fix generators for SEO issues
3
+ *
4
+ * These modules generate framework-specific code to fix SEO issues
5
+ * detected during audits.
6
+ */
7
+
8
+ import type { AuditReport, AuditIssue } from '../types.js';
9
+
10
+ // Alias for clarity
11
+ type SEOIssue = AuditIssue;
12
+
13
+ export * from './social-meta-fixes.js';
14
+
15
+ export interface FixFile {
16
+ path: string;
17
+ content: string;
18
+ }
19
+
20
+ export interface FixResult {
21
+ category: string;
22
+ issues: string[];
23
+ files: FixFile[];
24
+ }
25
+
26
+ /**
27
+ * Groups issues by category
28
+ */
29
+ function groupIssuesByCategory(issues: SEOIssue[]): Map<string, SEOIssue[]> {
30
+ const grouped = new Map<string, SEOIssue[]>();
31
+ for (const issue of issues) {
32
+ const existing = grouped.get(issue.category) || [];
33
+ existing.push(issue);
34
+ grouped.set(issue.category, existing);
35
+ }
36
+ return grouped;
37
+ }
38
+
39
+ /**
40
+ * Generates fixes for on-page issues (titles, meta descriptions, headings)
41
+ */
42
+ function generateOnPageFixes(issues: SEOIssue[]): FixResult | null {
43
+ if (issues.length === 0) return null;
44
+
45
+ const issueCodes = issues.map((i) => i.code);
46
+ const files: FixFile[] = [];
47
+
48
+ // Generate appropriate fixes based on detected issues
49
+ if (issueCodes.includes('TITLE_MISSING') || issueCodes.includes('META_DESC_MISSING')) {
50
+ files.push({
51
+ path: 'index.html',
52
+ content: `<head>
53
+ <title>Site Title - Your tagline here</title>
54
+ <meta name="description" content="A compelling description of your site in 150-160 characters." />
55
+ </head>`,
56
+ });
57
+ }
58
+
59
+ if (files.length === 0) return null;
60
+
61
+ return {
62
+ category: 'on-page',
63
+ issues: issueCodes,
64
+ files,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Generates fixes for social meta issues (Open Graph, Twitter Cards)
70
+ */
71
+ function generateSocialMetaFixes(issues: SEOIssue[]): FixResult | null {
72
+ if (issues.length === 0) return null;
73
+
74
+ const issueCodes = issues.map((i) => i.code);
75
+ const files: FixFile[] = [];
76
+
77
+ // Generate OG tags
78
+ if (issueCodes.some((c) => c.startsWith('OG_'))) {
79
+ files.push({
80
+ path: 'index.html',
81
+ content: `<meta property="og:title" content="Site Title" />
82
+ <meta property="og:description" content="Site description" />
83
+ <meta property="og:image" content="https://example.com/og-image.png" />
84
+ <meta property="og:url" content="https://example.com" />
85
+ <meta property="og:type" content="website" />`,
86
+ });
87
+ }
88
+
89
+ // Generate Twitter Card tags
90
+ if (issueCodes.some((c) => c.startsWith('TWITTER_'))) {
91
+ files.push({
92
+ path: 'index.html',
93
+ content: `<meta name="twitter:card" content="summary_large_image" />
94
+ <meta name="twitter:title" content="Site Title" />
95
+ <meta name="twitter:description" content="Site description" />
96
+ <meta name="twitter:image" content="https://example.com/twitter-image.png" />`,
97
+ });
98
+ }
99
+
100
+ if (files.length === 0) return null;
101
+
102
+ return {
103
+ category: 'social-meta',
104
+ issues: issueCodes,
105
+ files,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Generates fixes for structured data issues (JSON-LD, Schema.org)
111
+ */
112
+ function generateStructuredDataFixes(issues: SEOIssue[]): FixResult | null {
113
+ if (issues.length === 0) return null;
114
+
115
+ const issueCodes = issues.map((i) => i.code);
116
+ const files: FixFile[] = [];
117
+
118
+ if (issueCodes.includes('SCHEMA_MISSING')) {
119
+ files.push({
120
+ path: 'index.html',
121
+ content: `<script type="application/ld+json">
122
+ {
123
+ "@context": "https://schema.org",
124
+ "@type": "WebSite",
125
+ "name": "Site Name",
126
+ "url": "https://example.com"
127
+ }
128
+ </script>`,
129
+ });
130
+ }
131
+
132
+ if (files.length === 0) return null;
133
+
134
+ return {
135
+ category: 'structured-data',
136
+ issues: issueCodes,
137
+ files,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Generates fixes for crawlability issues (robots.txt, sitemap)
143
+ */
144
+ function generateCrawlabilityFixes(issues: SEOIssue[]): FixResult | null {
145
+ if (issues.length === 0) return null;
146
+
147
+ const issueCodes = issues.map((i) => i.code);
148
+ const files: FixFile[] = [];
149
+
150
+ if (issueCodes.includes('ROBOTS_TXT_MISSING')) {
151
+ files.push({
152
+ path: 'robots.txt',
153
+ content: `User-agent: *
154
+ Allow: /
155
+
156
+ Sitemap: https://example.com/sitemap.xml`,
157
+ });
158
+ }
159
+
160
+ if (issueCodes.includes('SITEMAP_MISSING')) {
161
+ files.push({
162
+ path: 'sitemap.xml',
163
+ content: `<?xml version="1.0" encoding="UTF-8"?>
164
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
165
+ <url>
166
+ <loc>https://example.com/</loc>
167
+ <lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
168
+ <changefreq>weekly</changefreq>
169
+ <priority>1.0</priority>
170
+ </url>
171
+ </urlset>`,
172
+ });
173
+ }
174
+
175
+ if (files.length === 0) return null;
176
+
177
+ return {
178
+ category: 'crawlability',
179
+ issues: issueCodes,
180
+ files,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Generates all fixes for issues found in an audit report
186
+ */
187
+ export function generateAllFixes(report: AuditReport): FixResult[] {
188
+ const fixes: FixResult[] = [];
189
+ const groupedIssues = groupIssuesByCategory(report.issues);
190
+
191
+ // Generate fixes for each category
192
+ const onPageIssues = groupedIssues.get('on-page') || [];
193
+ const onPageFix = generateOnPageFixes(onPageIssues);
194
+ if (onPageFix) fixes.push(onPageFix);
195
+
196
+ const socialMetaIssues = groupedIssues.get('social-meta') || [];
197
+ const socialMetaFix = generateSocialMetaFixes(socialMetaIssues);
198
+ if (socialMetaFix) fixes.push(socialMetaFix);
199
+
200
+ const structuredDataIssues = groupedIssues.get('structured-data') || [];
201
+ const structuredDataFix = generateStructuredDataFixes(structuredDataIssues);
202
+ if (structuredDataFix) fixes.push(structuredDataFix);
203
+
204
+ const crawlabilityIssues = groupedIssues.get('crawlability') || [];
205
+ const crawlabilityFix = generateCrawlabilityFixes(crawlabilityIssues);
206
+ if (crawlabilityFix) fixes.push(crawlabilityFix);
207
+
208
+ return fixes;
209
+ }
@@ -0,0 +1,329 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ detectFramework,
4
+ generateHTMLSocialMeta,
5
+ generateReactHelmetSocialMeta,
6
+ generateNextAppMetadata,
7
+ generateNextPagesHead,
8
+ generateAstroMeta,
9
+ generateRemixMeta,
10
+ generateSvelteKitMeta,
11
+ generateSocialMetaFix,
12
+ generateCompleteSocialMetaSetup,
13
+ OG_IMAGE_SPECS,
14
+ type SocialMetaConfig,
15
+ } from './social-meta-fixes.js';
16
+
17
+ describe('social-meta-fixes', () => {
18
+ const defaultConfig: SocialMetaConfig = {
19
+ title: 'Test Page',
20
+ description: 'A test description for SEO',
21
+ image: 'https://example.com/og.png',
22
+ url: 'https://example.com',
23
+ siteName: 'Example Site',
24
+ twitterSite: '@example',
25
+ twitterCreator: '@author',
26
+ type: 'website',
27
+ locale: 'en_US',
28
+ };
29
+
30
+ describe('detectFramework', () => {
31
+ it('detects Next.js App Router', () => {
32
+ // The path check looks for '/app/' or '\\app\\' in the file paths
33
+ const files = ['next.config.js', 'src/app/page.tsx', 'src/app/layout.tsx'];
34
+ expect(detectFramework(files)).toBe('nextjs-app');
35
+ });
36
+
37
+ it('detects Next.js Pages Router', () => {
38
+ const files = ['next.config.js', 'pages/index.tsx', 'pages/_app.tsx'];
39
+ expect(detectFramework(files)).toBe('nextjs-pages');
40
+ });
41
+
42
+ it('detects Astro', () => {
43
+ const files = ['astro.config.mjs', 'src/pages/index.astro'];
44
+ expect(detectFramework(files)).toBe('astro');
45
+ });
46
+
47
+ it('detects Remix', () => {
48
+ const files = ['remix.config.js', 'app/root.tsx'];
49
+ expect(detectFramework(files)).toBe('remix');
50
+ });
51
+
52
+ it('detects SvelteKit', () => {
53
+ const files = ['svelte.config.js', 'src/routes/+page.svelte'];
54
+ expect(detectFramework(files)).toBe('sveltekit');
55
+ });
56
+
57
+ it('detects Vite React', () => {
58
+ const files = ['vite.config.ts', 'src/App.tsx', 'index.html'];
59
+ expect(detectFramework(files)).toBe('vite-react');
60
+ });
61
+
62
+ it('detects plain React', () => {
63
+ const files = ['src/App.tsx', 'src/index.tsx'];
64
+ expect(detectFramework(files)).toBe('react');
65
+ });
66
+
67
+ it('defaults to HTML', () => {
68
+ const files = ['index.html', 'styles.css'];
69
+ expect(detectFramework(files)).toBe('html');
70
+ });
71
+
72
+ it('is case-insensitive for config files', () => {
73
+ // Config files are lowercased, but path check needs /app/ or \\app\\
74
+ const files = ['NEXT.CONFIG.JS', 'src/app/page.tsx'];
75
+ expect(detectFramework(files)).toBe('nextjs-app');
76
+ });
77
+ });
78
+
79
+ describe('generateHTMLSocialMeta', () => {
80
+ it('generates complete HTML meta tags', () => {
81
+ const result = generateHTMLSocialMeta(defaultConfig);
82
+
83
+ // Primary meta tags
84
+ expect(result).toContain('<title>Test Page</title>');
85
+ expect(result).toContain('<meta name="title" content="Test Page" />');
86
+ expect(result).toContain('<meta name="description" content="A test description for SEO" />');
87
+
88
+ // Open Graph tags
89
+ expect(result).toContain('<meta property="og:type" content="website" />');
90
+ expect(result).toContain('<meta property="og:url" content="https://example.com" />');
91
+ expect(result).toContain('<meta property="og:title" content="Test Page" />');
92
+ expect(result).toContain('<meta property="og:image" content="https://example.com/og.png" />');
93
+ expect(result).toContain('<meta property="og:site_name" content="Example Site" />');
94
+ expect(result).toContain('<meta property="og:locale" content="en_US" />');
95
+
96
+ // Twitter tags
97
+ expect(result).toContain('<meta name="twitter:card" content="summary_large_image" />');
98
+ expect(result).toContain('<meta name="twitter:title" content="Test Page" />');
99
+ expect(result).toContain('<meta name="twitter:site" content="@example" />');
100
+ expect(result).toContain('<meta name="twitter:creator" content="@author" />');
101
+ });
102
+
103
+ it('omits optional fields when not provided', () => {
104
+ const minimalConfig: SocialMetaConfig = {
105
+ title: 'Minimal',
106
+ description: 'Minimal description',
107
+ image: 'https://example.com/og.png',
108
+ url: 'https://example.com',
109
+ };
110
+ const result = generateHTMLSocialMeta(minimalConfig);
111
+
112
+ expect(result).not.toContain('og:site_name');
113
+ expect(result).not.toContain('twitter:site');
114
+ expect(result).not.toContain('twitter:creator');
115
+ });
116
+ });
117
+
118
+ describe('generateReactHelmetSocialMeta', () => {
119
+ it('generates React Helmet component', () => {
120
+ const result = generateReactHelmetSocialMeta(defaultConfig);
121
+
122
+ expect(result).toContain("import { Helmet } from 'react-helmet-async'");
123
+ expect(result).toContain('export function SEO');
124
+ expect(result).toContain('<Helmet>');
125
+ expect(result).toContain('<title>{title}</title>');
126
+ expect(result).toContain('og:type');
127
+ expect(result).toContain('twitter:card');
128
+ });
129
+
130
+ it('includes default props', () => {
131
+ const result = generateReactHelmetSocialMeta(defaultConfig);
132
+
133
+ expect(result).toContain('title = "Test Page"');
134
+ expect(result).toContain('description = "A test description for SEO"');
135
+ });
136
+ });
137
+
138
+ describe('generateNextAppMetadata', () => {
139
+ it('generates Next.js App Router metadata', () => {
140
+ const result = generateNextAppMetadata(defaultConfig);
141
+
142
+ expect(result).toContain("import type { Metadata } from 'next'");
143
+ expect(result).toContain('export const metadata: Metadata');
144
+ expect(result).toContain('title: "Test Page"');
145
+ expect(result).toContain('openGraph:');
146
+ expect(result).toContain('twitter:');
147
+ expect(result).toContain("card: 'summary_large_image'");
148
+ });
149
+
150
+ it('includes image dimensions', () => {
151
+ const result = generateNextAppMetadata(defaultConfig);
152
+
153
+ expect(result).toContain('width: 1200');
154
+ expect(result).toContain('height: 630');
155
+ });
156
+ });
157
+
158
+ describe('generateNextPagesHead', () => {
159
+ it('generates Next.js Pages Router Head component', () => {
160
+ const result = generateNextPagesHead(defaultConfig);
161
+
162
+ expect(result).toContain("import Head from 'next/head'");
163
+ expect(result).toContain('<Head>');
164
+ expect(result).toContain('<title>Test Page</title>');
165
+ expect(result).toContain('og:title');
166
+ expect(result).toContain('twitter:card');
167
+ });
168
+ });
169
+
170
+ describe('generateAstroMeta', () => {
171
+ it('generates Astro component with frontmatter', () => {
172
+ const result = generateAstroMeta(defaultConfig);
173
+
174
+ expect(result).toContain('---');
175
+ expect(result).toContain('export interface Props');
176
+ expect(result).toContain('Astro.props');
177
+ expect(result).toContain('<title>{title}</title>');
178
+ expect(result).toContain('og:type');
179
+ expect(result).toContain('twitter:card');
180
+ });
181
+
182
+ it('uses Astro.url for dynamic URL', () => {
183
+ const result = generateAstroMeta(defaultConfig);
184
+
185
+ expect(result).toContain('url = Astro.url.href');
186
+ });
187
+ });
188
+
189
+ describe('generateRemixMeta', () => {
190
+ it('generates Remix meta function', () => {
191
+ const result = generateRemixMeta(defaultConfig);
192
+
193
+ expect(result).toContain("import type { MetaFunction } from \"@remix-run/node\"");
194
+ expect(result).toContain('export const meta: MetaFunction');
195
+ expect(result).toContain('return [');
196
+ expect(result).toContain('{ title: "Test Page" }');
197
+ expect(result).toContain('{ property: "og:title"');
198
+ expect(result).toContain('{ name: "twitter:card"');
199
+ });
200
+ });
201
+
202
+ describe('generateSvelteKitMeta', () => {
203
+ it('generates SvelteKit svelte:head', () => {
204
+ const result = generateSvelteKitMeta(defaultConfig);
205
+
206
+ expect(result).toContain('<svelte:head>');
207
+ expect(result).toContain('<title>Test Page</title>');
208
+ expect(result).toContain('og:type');
209
+ expect(result).toContain('twitter:card');
210
+ });
211
+ });
212
+
213
+ describe('generateSocialMetaFix', () => {
214
+ const existingData = {
215
+ openGraph: {},
216
+ twitter: {},
217
+ hasFavicon: false,
218
+ };
219
+
220
+ it('generates fix for missing OG title', () => {
221
+ const fix = generateSocialMetaFix(
222
+ 'OG_TITLE_MISSING',
223
+ 'html',
224
+ existingData,
225
+ { url: 'https://example.com', title: 'My Page' }
226
+ );
227
+
228
+ expect(fix.issueCode).toBe('OG_TITLE_MISSING');
229
+ expect(fix.framework).toBe('html');
230
+ expect(fix.filePath).toBe('index.html');
231
+ expect(fix.after).toContain('og:title');
232
+ expect(fix.explanation).toContain('Open Graph title');
233
+ });
234
+
235
+ it('generates fix for missing Twitter Card', () => {
236
+ const fix = generateSocialMetaFix(
237
+ 'TWITTER_CARD_MISSING',
238
+ 'react',
239
+ existingData,
240
+ { url: 'https://example.com' }
241
+ );
242
+
243
+ expect(fix.issueCode).toBe('TWITTER_CARD_MISSING');
244
+ expect(fix.filePath).toBe('src/components/SEO.tsx');
245
+ expect(fix.after).toContain('twitter:card');
246
+ expect(fix.after).toContain('summary_large_image');
247
+ });
248
+
249
+ it('uses correct file paths for each framework', () => {
250
+ const frameworks = [
251
+ { framework: 'html' as const, expectedPath: 'index.html' },
252
+ { framework: 'react' as const, expectedPath: 'src/components/SEO.tsx' },
253
+ { framework: 'vite-react' as const, expectedPath: 'index.html' },
254
+ { framework: 'nextjs-app' as const, expectedPath: 'app/layout.tsx' },
255
+ { framework: 'nextjs-pages' as const, expectedPath: 'pages/_app.tsx' },
256
+ { framework: 'astro' as const, expectedPath: 'src/components/BaseHead.astro' },
257
+ { framework: 'remix' as const, expectedPath: 'app/root.tsx' },
258
+ { framework: 'sveltekit' as const, expectedPath: 'src/routes/+layout.svelte' },
259
+ ];
260
+
261
+ frameworks.forEach(({ framework, expectedPath }) => {
262
+ const fix = generateSocialMetaFix(
263
+ 'OG_TITLE_MISSING',
264
+ framework,
265
+ existingData,
266
+ { url: 'https://example.com' }
267
+ );
268
+ expect(fix.filePath).toBe(expectedPath);
269
+ });
270
+ });
271
+
272
+ it('throws error for unknown issue code', () => {
273
+ expect(() =>
274
+ generateSocialMetaFix(
275
+ 'UNKNOWN_ISSUE',
276
+ 'html',
277
+ existingData,
278
+ { url: 'https://example.com' }
279
+ )
280
+ ).toThrow('Unknown issue code: UNKNOWN_ISSUE');
281
+ });
282
+ });
283
+
284
+ describe('generateCompleteSocialMetaSetup', () => {
285
+ it('generates complete setup for each framework', () => {
286
+ const frameworks = [
287
+ 'html',
288
+ 'react',
289
+ 'vite-react',
290
+ 'nextjs-app',
291
+ 'nextjs-pages',
292
+ 'astro',
293
+ 'remix',
294
+ 'sveltekit',
295
+ ] as const;
296
+
297
+ frameworks.forEach((framework) => {
298
+ const result = generateCompleteSocialMetaSetup(framework, defaultConfig);
299
+
300
+ expect(result).toHaveProperty('filePath');
301
+ expect(result).toHaveProperty('content');
302
+ expect(result).toHaveProperty('explanation');
303
+ expect(result.content.length).toBeGreaterThan(0);
304
+ expect(result.explanation).toContain(framework);
305
+ });
306
+ });
307
+ });
308
+
309
+ describe('OG_IMAGE_SPECS', () => {
310
+ it('has recommended dimensions', () => {
311
+ expect(OG_IMAGE_SPECS.recommended.width).toBe(1200);
312
+ expect(OG_IMAGE_SPECS.recommended.height).toBe(630);
313
+ });
314
+
315
+ it('has Twitter specs', () => {
316
+ expect(OG_IMAGE_SPECS.twitter.summary_large_image.width).toBe(1200);
317
+ expect(OG_IMAGE_SPECS.twitter.summary_large_image.height).toBe(600);
318
+ });
319
+
320
+ it('has Facebook specs', () => {
321
+ expect(OG_IMAGE_SPECS.facebook.recommended.width).toBe(1200);
322
+ expect(OG_IMAGE_SPECS.facebook.recommended.height).toBe(630);
323
+ });
324
+
325
+ it('has LinkedIn specs', () => {
326
+ expect(OG_IMAGE_SPECS.linkedin.recommended.width).toBe(1200);
327
+ });
328
+ });
329
+ });