@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,320 @@
1
+ /**
2
+ * Framework-Specific SEO Suggestion Engine
3
+ *
4
+ * Loads recipe files and provides framework-specific SEO suggestions.
5
+ * Works with both CLI and Web UI.
6
+ */
7
+
8
+ import { parse as parseYaml } from 'yaml';
9
+ import type { Framework, FrameworkInfo } from './detector.js';
10
+ import type { AuditIssue, IssueCategory, IssueSeverity } from '../audit/types.js';
11
+
12
+ export interface FrameworkCheck {
13
+ code: string;
14
+ severity: IssueSeverity;
15
+ title: string;
16
+ description: string;
17
+ howToFix: string;
18
+ impact?: string;
19
+ detection?: {
20
+ html_patterns?: string[];
21
+ missing_content?: boolean;
22
+ title_in_js?: boolean;
23
+ title_not_in_html?: boolean;
24
+ };
25
+ app_router_only?: boolean;
26
+ pages_router_only?: boolean;
27
+ }
28
+
29
+ export interface FrameworkRecipe {
30
+ framework: Framework;
31
+ display_name: string;
32
+ category: string;
33
+ base_framework?: Framework;
34
+ ssr_default: boolean;
35
+ seo_challenges?: string[];
36
+ seo_strengths?: string[];
37
+ checks: FrameworkCheck[];
38
+ performance_tips?: string[];
39
+ migration_paths?: Record<string, {
40
+ effort: string;
41
+ benefits: string[];
42
+ guide?: string;
43
+ }>;
44
+ recommended_plugins?: Record<string, string[]>;
45
+ recommended_gems?: Record<string, string[]>;
46
+ integrations?: string[];
47
+ }
48
+
49
+ // Embedded recipes (loaded at build time or dynamically)
50
+ const RECIPES: Record<Framework, FrameworkRecipe | null> = {
51
+ react: null,
52
+ nextjs: null,
53
+ vue: null,
54
+ nuxt: null,
55
+ angular: null,
56
+ svelte: null,
57
+ sveltekit: null,
58
+ astro: null,
59
+ remix: null,
60
+ gatsby: null,
61
+ rails: null,
62
+ laravel: null,
63
+ django: null,
64
+ wordpress: null,
65
+ unknown: null,
66
+ };
67
+
68
+ // Recipe content will be bundled inline for distribution
69
+ // This allows the suggestion engine to work without file system access
70
+ const RECIPE_CONTENT: Record<string, string> = {};
71
+
72
+ /**
73
+ * Load a recipe from embedded content or file system
74
+ */
75
+ export async function loadRecipe(framework: Framework): Promise<FrameworkRecipe | null> {
76
+ if (framework === 'unknown') return null;
77
+
78
+ // Check cache
79
+ if (RECIPES[framework]) {
80
+ return RECIPES[framework];
81
+ }
82
+
83
+ // Check embedded content
84
+ if (RECIPE_CONTENT[framework]) {
85
+ try {
86
+ const recipe = parseYaml(RECIPE_CONTENT[framework]) as FrameworkRecipe;
87
+ RECIPES[framework] = recipe;
88
+ return recipe;
89
+ } catch {
90
+ console.warn(`Failed to parse embedded recipe for ${framework}`);
91
+ return null;
92
+ }
93
+ }
94
+
95
+ // Try dynamic import for Node.js environment
96
+ try {
97
+ const fs = await import('fs');
98
+ const path = await import('path');
99
+ const url = await import('url');
100
+
101
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
102
+ const recipePath = path.join(__dirname, 'recipes', `${framework}.yaml`);
103
+
104
+ if (fs.existsSync(recipePath)) {
105
+ const content = fs.readFileSync(recipePath, 'utf-8');
106
+ const recipe = parseYaml(content) as FrameworkRecipe;
107
+ RECIPES[framework] = recipe;
108
+ return recipe;
109
+ }
110
+ } catch {
111
+ // File system not available (browser, edge function)
112
+ }
113
+
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Get framework-specific issues based on HTML analysis
119
+ */
120
+ export async function getFrameworkIssues(
121
+ frameworkInfo: FrameworkInfo,
122
+ html: string,
123
+ url: string
124
+ ): Promise<AuditIssue[]> {
125
+ const recipe = await loadRecipe(frameworkInfo.framework);
126
+ if (!recipe) return [];
127
+
128
+ const issues: AuditIssue[] = [];
129
+
130
+ for (const check of recipe.checks) {
131
+ const matched = evaluateCheck(check, html, frameworkInfo);
132
+ if (matched) {
133
+ issues.push({
134
+ code: check.code,
135
+ severity: check.severity,
136
+ category: 'on-page' as IssueCategory, // Framework issues are on-page
137
+ title: check.title,
138
+ description: check.description,
139
+ impact: check.impact || `Framework-specific issue for ${recipe.display_name}`,
140
+ howToFix: check.howToFix,
141
+ affectedUrls: [url],
142
+ details: {
143
+ framework: recipe.display_name,
144
+ frameworkCategory: recipe.category,
145
+ },
146
+ });
147
+ }
148
+ }
149
+
150
+ return issues;
151
+ }
152
+
153
+ /**
154
+ * Evaluate if a check matches the HTML content
155
+ */
156
+ function evaluateCheck(
157
+ check: FrameworkCheck,
158
+ html: string,
159
+ frameworkInfo: FrameworkInfo
160
+ ): boolean {
161
+ // Skip router-specific checks
162
+ if (check.app_router_only && !html.includes('__next')) {
163
+ return false;
164
+ }
165
+ if (check.pages_router_only && html.includes('__next')) {
166
+ return false;
167
+ }
168
+
169
+ const detection = check.detection;
170
+ if (!detection) {
171
+ // No detection rules, check is informational only
172
+ return false;
173
+ }
174
+
175
+ // Check HTML patterns
176
+ if (detection.html_patterns) {
177
+ for (const pattern of detection.html_patterns) {
178
+ if (html.includes(pattern)) {
179
+ return true;
180
+ }
181
+ }
182
+ }
183
+
184
+ // Check for missing content (SPA detection)
185
+ if (detection.missing_content) {
186
+ const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
187
+ if (bodyMatch) {
188
+ const bodyContent = bodyMatch[1]
189
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
190
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
191
+ .replace(/<[^>]+>/g, '')
192
+ .replace(/\s+/g, ' ')
193
+ .trim();
194
+
195
+ // Less than 100 chars of text content = likely empty SPA
196
+ if (bodyContent.length < 100) {
197
+ return true;
198
+ }
199
+ }
200
+ }
201
+
202
+ // Check title in JS but not in HTML
203
+ if (detection.title_in_js && detection.title_not_in_html) {
204
+ const hasHtmlTitle = /<title[^>]*>[^<]+<\/title>/i.test(html);
205
+ const hasJsTitle = /document\.title\s*=/.test(html) ||
206
+ /useHead|Helmet|useSeoMeta/.test(html);
207
+ if (!hasHtmlTitle && hasJsTitle) {
208
+ return true;
209
+ }
210
+ }
211
+
212
+ return false;
213
+ }
214
+
215
+ /**
216
+ * Get framework-specific suggestions for fixing issues
217
+ */
218
+ export async function getFrameworkSuggestions(
219
+ frameworkInfo: FrameworkInfo,
220
+ issueCode: string
221
+ ): Promise<string | null> {
222
+ const recipe = await loadRecipe(frameworkInfo.framework);
223
+ if (!recipe) return null;
224
+
225
+ const check = recipe.checks.find(c => c.code === issueCode);
226
+ if (check) {
227
+ return check.howToFix;
228
+ }
229
+
230
+ return null;
231
+ }
232
+
233
+ /**
234
+ * Get all available checks for a framework
235
+ */
236
+ export async function getFrameworkChecks(
237
+ framework: Framework
238
+ ): Promise<FrameworkCheck[]> {
239
+ const recipe = await loadRecipe(framework);
240
+ if (!recipe) return [];
241
+ return recipe.checks;
242
+ }
243
+
244
+ /**
245
+ * Get performance tips for a framework
246
+ */
247
+ export async function getPerformanceTips(
248
+ framework: Framework
249
+ ): Promise<string[]> {
250
+ const recipe = await loadRecipe(framework);
251
+ if (!recipe) return [];
252
+ return recipe.performance_tips || [];
253
+ }
254
+
255
+ /**
256
+ * Get SEO challenges for a framework
257
+ */
258
+ export async function getSeoChallenges(
259
+ framework: Framework
260
+ ): Promise<string[]> {
261
+ const recipe = await loadRecipe(framework);
262
+ if (!recipe) return [];
263
+ return recipe.seo_challenges || [];
264
+ }
265
+
266
+ /**
267
+ * Get migration paths from current framework
268
+ */
269
+ export async function getMigrationPaths(
270
+ framework: Framework
271
+ ): Promise<Record<string, { effort: string; benefits: string[]; guide?: string }>> {
272
+ const recipe = await loadRecipe(framework);
273
+ if (!recipe) return {};
274
+ return recipe.migration_paths || {};
275
+ }
276
+
277
+ /**
278
+ * Get total number of framework-specific checks
279
+ */
280
+ export async function getTotalFrameworkChecks(): Promise<number> {
281
+ let total = 0;
282
+ for (const framework of Object.keys(RECIPES) as Framework[]) {
283
+ const recipe = await loadRecipe(framework);
284
+ if (recipe) {
285
+ total += recipe.checks.length;
286
+ }
287
+ }
288
+ return total;
289
+ }
290
+
291
+ /**
292
+ * Get framework summary for display
293
+ */
294
+ export async function getFrameworkSummary(framework: Framework): Promise<{
295
+ name: string;
296
+ category: string;
297
+ ssrDefault: boolean;
298
+ checkCount: number;
299
+ strengths: string[];
300
+ challenges: string[];
301
+ } | null> {
302
+ const recipe = await loadRecipe(framework);
303
+ if (!recipe) return null;
304
+
305
+ return {
306
+ name: recipe.display_name,
307
+ category: recipe.category,
308
+ ssrDefault: recipe.ssr_default,
309
+ checkCount: recipe.checks.length,
310
+ strengths: recipe.seo_strengths || [],
311
+ challenges: recipe.seo_challenges || [],
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Embed recipe content for bundling (called at build time)
317
+ */
318
+ export function embedRecipe(framework: Framework, content: string): void {
319
+ RECIPE_CONTENT[framework] = content;
320
+ }
@@ -0,0 +1,305 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ generateAICitableContent,
4
+ generateFAQSchema,
5
+ generateComparisonTable,
6
+ generateKeyFacts,
7
+ optimizeForAI,
8
+ AIContentConfig,
9
+ FAQItem,
10
+ ComparisonRow,
11
+ KeyFact,
12
+ } from './geo-content.js';
13
+
14
+ describe('geo-content', () => {
15
+ describe('generateKeyFacts', () => {
16
+ it('formats facts with bold markers', () => {
17
+ const facts: KeyFact[] = [
18
+ { label: 'Users', value: '10,000+' },
19
+ { label: 'Checks', value: '200+' },
20
+ ];
21
+
22
+ const content = generateKeyFacts(facts);
23
+
24
+ expect(content).toContain('**Users:** 10,000+');
25
+ expect(content).toContain('**Checks:** 200+');
26
+ });
27
+
28
+ it('generates bullet list format', () => {
29
+ const facts: KeyFact[] = [
30
+ { label: 'Speed', value: 'Fast' },
31
+ { label: 'Price', value: 'Free' },
32
+ ];
33
+
34
+ const content = generateKeyFacts(facts, { format: 'bullets' });
35
+
36
+ expect(content).toContain('- **Speed:** Fast');
37
+ expect(content).toContain('- **Price:** Free');
38
+ });
39
+
40
+ it('generates paragraph format', () => {
41
+ const facts: KeyFact[] = [
42
+ { label: 'Founded', value: '2024' },
43
+ ];
44
+
45
+ const content = generateKeyFacts(facts, { format: 'paragraph' });
46
+
47
+ expect(content).not.toContain('- ');
48
+ expect(content).toContain('**Founded:** 2024');
49
+ });
50
+ });
51
+
52
+ describe('generateFAQSchema', () => {
53
+ it('generates valid FAQ schema JSON-LD', () => {
54
+ const faqs: FAQItem[] = [
55
+ { question: 'What is SEO Autopilot?', answer: 'A CLI-first SEO tool.' },
56
+ { question: 'How much does it cost?', answer: 'Free tier available.' },
57
+ ];
58
+
59
+ const schema = generateFAQSchema(faqs);
60
+ const parsed = JSON.parse(schema);
61
+
62
+ expect(parsed['@context']).toBe('https://schema.org');
63
+ expect(parsed['@type']).toBe('FAQPage');
64
+ expect(parsed.mainEntity).toHaveLength(2);
65
+ expect(parsed.mainEntity[0]['@type']).toBe('Question');
66
+ });
67
+
68
+ it('escapes special characters in JSON', () => {
69
+ const faqs: FAQItem[] = [
70
+ { question: 'What about "quotes"?', answer: 'They work fine.' },
71
+ ];
72
+
73
+ const schema = generateFAQSchema(faqs);
74
+
75
+ // Should be valid JSON
76
+ expect(() => JSON.parse(schema)).not.toThrow();
77
+ });
78
+
79
+ it('generates markdown FAQ section', () => {
80
+ const faqs: FAQItem[] = [
81
+ { question: 'How do I install?', answer: 'Run npm install.' },
82
+ ];
83
+
84
+ const markdown = generateFAQSchema(faqs, { format: 'markdown' });
85
+
86
+ expect(markdown).toContain('## FAQ');
87
+ expect(markdown).toContain('### How do I install?');
88
+ expect(markdown).toContain('Run npm install.');
89
+ });
90
+ });
91
+
92
+ describe('generateComparisonTable', () => {
93
+ it('generates markdown table', () => {
94
+ const rows: ComparisonRow[] = [
95
+ { feature: 'CLI Tool', brand: 'Yes', competitor: 'No' },
96
+ { feature: 'Auto-fix PRs', brand: 'Yes', competitor: 'No' },
97
+ ];
98
+
99
+ const table = generateComparisonTable(rows, {
100
+ brandName: 'SEO Autopilot',
101
+ competitorName: 'Ahrefs',
102
+ });
103
+
104
+ expect(table).toContain('| Feature | SEO Autopilot | Ahrefs |');
105
+ expect(table).toContain('| CLI Tool | Yes | No |');
106
+ });
107
+
108
+ it('uses checkmarks when specified', () => {
109
+ const rows: ComparisonRow[] = [
110
+ { feature: 'Free Tier', brand: true, competitor: false },
111
+ ];
112
+
113
+ const table = generateComparisonTable(rows, {
114
+ brandName: 'Us',
115
+ competitorName: 'Them',
116
+ useCheckmarks: true,
117
+ });
118
+
119
+ expect(table).toMatch(/✓|✅/);
120
+ expect(table).toMatch(/✗|❌|-/);
121
+ });
122
+
123
+ it('handles multiple competitors', () => {
124
+ const rows: ComparisonRow[] = [
125
+ { feature: 'Price', brand: '$29', competitors: { 'Ahrefs': '$99', 'SEMrush': '$129' } },
126
+ ];
127
+
128
+ const table = generateComparisonTable(rows, {
129
+ brandName: 'SEO Autopilot',
130
+ competitorNames: ['Ahrefs', 'SEMrush'],
131
+ });
132
+
133
+ expect(table).toContain('Ahrefs');
134
+ expect(table).toContain('SEMrush');
135
+ expect(table).toContain('$99');
136
+ expect(table).toContain('$129');
137
+ });
138
+ });
139
+
140
+ describe('generateAICitableContent', () => {
141
+ it('generates structured content block', () => {
142
+ const config: AIContentConfig = {
143
+ productName: 'SEO Autopilot',
144
+ tagline: 'CLI-first SEO automation',
145
+ keyFacts: [
146
+ { label: 'Checks', value: '200+' },
147
+ ],
148
+ };
149
+
150
+ const content = generateAICitableContent(config);
151
+
152
+ expect(content).toContain('SEO Autopilot');
153
+ expect(content).toContain('CLI-first SEO automation');
154
+ expect(content).toContain('**Checks:** 200+');
155
+ });
156
+
157
+ it('includes FAQ section when provided', () => {
158
+ const config: AIContentConfig = {
159
+ productName: 'Test Product',
160
+ faqs: [
161
+ { question: 'What is it?', answer: 'A great product.' },
162
+ ],
163
+ };
164
+
165
+ const content = generateAICitableContent(config);
166
+
167
+ expect(content).toContain('What is it?');
168
+ expect(content).toContain('A great product.');
169
+ });
170
+
171
+ it('includes comparison table when provided', () => {
172
+ const config: AIContentConfig = {
173
+ productName: 'SEO Autopilot',
174
+ comparison: {
175
+ competitorName: 'Competitor',
176
+ rows: [
177
+ { feature: 'CLI', brand: 'Yes', competitor: 'No' },
178
+ ],
179
+ },
180
+ };
181
+
182
+ const content = generateAICitableContent(config);
183
+
184
+ expect(content).toContain('CLI');
185
+ expect(content).toContain('Yes');
186
+ expect(content).toContain('No');
187
+ });
188
+
189
+ it('generates all sections in correct order', () => {
190
+ const config: AIContentConfig = {
191
+ productName: 'SEO Autopilot',
192
+ tagline: 'The developer SEO tool',
193
+ description: 'Full description here.',
194
+ keyFacts: [{ label: 'Users', value: '5000+' }],
195
+ faqs: [{ question: 'Q?', answer: 'A.' }],
196
+ };
197
+
198
+ const content = generateAICitableContent(config);
199
+
200
+ const taglineIndex = content.indexOf('developer SEO tool');
201
+ const factsIndex = content.indexOf('Users');
202
+ const faqIndex = content.indexOf('Q?');
203
+
204
+ expect(taglineIndex).toBeLessThan(factsIndex);
205
+ expect(factsIndex).toBeLessThan(faqIndex);
206
+ });
207
+ });
208
+
209
+ describe('optimizeForAI', () => {
210
+ it('adds citable markers to key statements', () => {
211
+ const content = 'SEO Autopilot runs 200 checks on your website.';
212
+
213
+ const optimized = optimizeForAI(content, {
214
+ highlightNumbers: true,
215
+ });
216
+
217
+ expect(optimized).toContain('**200**');
218
+ });
219
+
220
+ it('converts lists to structured format', () => {
221
+ const content = `Features include:
222
+ speed, accuracy, and simplicity.`;
223
+
224
+ const optimized = optimizeForAI(content, {
225
+ structureLists: true,
226
+ });
227
+
228
+ expect(optimized).toContain('- ');
229
+ });
230
+
231
+ it('adds emphasis to brand mentions', () => {
232
+ const content = 'Use SEO Autopilot for better rankings.';
233
+
234
+ const optimized = optimizeForAI(content, {
235
+ brandName: 'SEO Autopilot',
236
+ emphasizeBrand: true,
237
+ });
238
+
239
+ expect(optimized).toContain('**SEO Autopilot**');
240
+ });
241
+
242
+ it('preserves existing markdown', () => {
243
+ const content = 'This is **already bold** text.';
244
+
245
+ const optimized = optimizeForAI(content, {
246
+ highlightNumbers: true,
247
+ });
248
+
249
+ expect(optimized).toContain('**already bold**');
250
+ });
251
+
252
+ it('adds source attribution placeholder', () => {
253
+ const content = 'Some facts here.';
254
+
255
+ const optimized = optimizeForAI(content, {
256
+ addSourcePlaceholder: true,
257
+ });
258
+
259
+ expect(optimized).toContain('Source:');
260
+ });
261
+ });
262
+
263
+ describe('integration', () => {
264
+ it('generates complete AI-optimized landing content', () => {
265
+ const config: AIContentConfig = {
266
+ productName: 'SEO Autopilot',
267
+ tagline: 'The CLI-First SEO Tool for Developers',
268
+ description: 'SEO Autopilot runs 200+ automated SEO checks directly from your terminal, making it the only developer-native SEO tool on the market.',
269
+ keyFacts: [
270
+ { label: 'Automated Checks', value: '200+' },
271
+ { label: 'GitHub Stars', value: '5,000+' },
272
+ { label: 'Monthly Downloads', value: '50,000+' },
273
+ ],
274
+ faqs: [
275
+ {
276
+ question: 'What is SEO Autopilot?',
277
+ answer: 'SEO Autopilot is a CLI-first SEO automation tool designed specifically for developers. It runs 200+ checks and can auto-fix issues via pull requests.',
278
+ },
279
+ {
280
+ question: 'How much does SEO Autopilot cost?',
281
+ answer: 'SEO Autopilot offers a free tier with 50 checks. Paid plans start at $9/month for Solo developers and $29/month for Pro teams.',
282
+ },
283
+ ],
284
+ comparison: {
285
+ competitorName: 'Ahrefs',
286
+ rows: [
287
+ { feature: 'CLI Tool', brand: 'Yes', competitor: 'No' },
288
+ { feature: 'Auto-fix PRs', brand: 'Yes', competitor: 'No' },
289
+ { feature: 'Free Tier', brand: 'Yes', competitor: 'No' },
290
+ { feature: 'Starting Price', brand: '$9/mo', competitor: '$99/mo' },
291
+ ],
292
+ },
293
+ };
294
+
295
+ const content = generateAICitableContent(config);
296
+
297
+ // Should have all major sections
298
+ expect(content).toContain('SEO Autopilot');
299
+ expect(content).toContain('200+');
300
+ expect(content).toContain('What is SEO Autopilot?');
301
+ expect(content).toContain('CLI Tool');
302
+ expect(content).toContain('$9/mo');
303
+ });
304
+ });
305
+ });