@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.
- package/README.md +242 -0
- package/dist/analyzer-2CSWIQGD.mjs +6 -0
- package/dist/chunk-YNZYHEYM.mjs +774 -0
- package/dist/index.d.mts +4012 -0
- package/dist/index.d.ts +4012 -0
- package/dist/index.js +29672 -0
- package/dist/index.mjs +28602 -0
- package/package.json +53 -0
- package/scripts/build-deno.ts +134 -0
- package/src/audit/ai/analyzer.ts +347 -0
- package/src/audit/ai/index.ts +29 -0
- package/src/audit/ai/prompts/content-analysis.ts +271 -0
- package/src/audit/ai/types.ts +179 -0
- package/src/audit/checks/additional-checks.ts +439 -0
- package/src/audit/checks/ai-citation-worthiness.ts +399 -0
- package/src/audit/checks/ai-content-structure.ts +325 -0
- package/src/audit/checks/ai-readiness.ts +339 -0
- package/src/audit/checks/anchor-text.ts +179 -0
- package/src/audit/checks/answer-conciseness.ts +322 -0
- package/src/audit/checks/asset-minification.ts +270 -0
- package/src/audit/checks/bing-optimization.ts +206 -0
- package/src/audit/checks/brand-mention-optimization.ts +349 -0
- package/src/audit/checks/caching-headers.ts +305 -0
- package/src/audit/checks/canonical-advanced.ts +150 -0
- package/src/audit/checks/canonical-domain.ts +196 -0
- package/src/audit/checks/citation-quality.ts +358 -0
- package/src/audit/checks/client-rendering.ts +542 -0
- package/src/audit/checks/color-contrast.ts +342 -0
- package/src/audit/checks/content-freshness.ts +170 -0
- package/src/audit/checks/content-science.ts +589 -0
- package/src/audit/checks/conversion-elements.ts +526 -0
- package/src/audit/checks/crawlability.ts +220 -0
- package/src/audit/checks/directory-listing.ts +172 -0
- package/src/audit/checks/dom-analysis.ts +191 -0
- package/src/audit/checks/dom-size.ts +246 -0
- package/src/audit/checks/duplicate-content.ts +194 -0
- package/src/audit/checks/eeat-signals.ts +990 -0
- package/src/audit/checks/entity-seo.ts +396 -0
- package/src/audit/checks/featured-snippet.ts +473 -0
- package/src/audit/checks/freshness-signals.ts +443 -0
- package/src/audit/checks/funnel-intent.ts +463 -0
- package/src/audit/checks/hreflang.ts +174 -0
- package/src/audit/checks/html-compliance.ts +302 -0
- package/src/audit/checks/image-dimensions.ts +167 -0
- package/src/audit/checks/images.ts +160 -0
- package/src/audit/checks/indexnow.ts +275 -0
- package/src/audit/checks/interactive-tools.ts +475 -0
- package/src/audit/checks/internal-link-graph.ts +436 -0
- package/src/audit/checks/keyword-analysis.ts +239 -0
- package/src/audit/checks/keyword-cannibalization.ts +385 -0
- package/src/audit/checks/keyword-placement.ts +471 -0
- package/src/audit/checks/links.ts +203 -0
- package/src/audit/checks/llms-txt.ts +224 -0
- package/src/audit/checks/local-seo.ts +296 -0
- package/src/audit/checks/mobile.ts +167 -0
- package/src/audit/checks/modern-images.ts +226 -0
- package/src/audit/checks/navboost-signals.ts +395 -0
- package/src/audit/checks/on-page.ts +209 -0
- package/src/audit/checks/page-resources.ts +285 -0
- package/src/audit/checks/pagination.ts +180 -0
- package/src/audit/checks/performance.ts +153 -0
- package/src/audit/checks/platform-presence.ts +580 -0
- package/src/audit/checks/redirect-analysis.ts +153 -0
- package/src/audit/checks/redirect-chain.ts +389 -0
- package/src/audit/checks/resource-hints.ts +420 -0
- package/src/audit/checks/responsive-css.ts +247 -0
- package/src/audit/checks/responsive-images.ts +396 -0
- package/src/audit/checks/review-ecosystem.ts +415 -0
- package/src/audit/checks/robots-validation.ts +373 -0
- package/src/audit/checks/security-headers.ts +172 -0
- package/src/audit/checks/security.ts +144 -0
- package/src/audit/checks/serp-preview.ts +251 -0
- package/src/audit/checks/site-maturity.ts +444 -0
- package/src/audit/checks/social-meta.test.ts +275 -0
- package/src/audit/checks/social-meta.ts +134 -0
- package/src/audit/checks/soft-404.ts +151 -0
- package/src/audit/checks/structured-data.ts +238 -0
- package/src/audit/checks/tech-detection.ts +496 -0
- package/src/audit/checks/topical-clusters.ts +435 -0
- package/src/audit/checks/tracker-bloat.ts +462 -0
- package/src/audit/checks/tracking-verification.test.ts +371 -0
- package/src/audit/checks/tracking-verification.ts +636 -0
- package/src/audit/checks/url-safety.ts +682 -0
- package/src/audit/deno-entry.ts +66 -0
- package/src/audit/discovery/index.ts +15 -0
- package/src/audit/discovery/link-crawler.ts +232 -0
- package/src/audit/discovery/repo-routes.ts +347 -0
- package/src/audit/engine.ts +620 -0
- package/src/audit/fixes/index.ts +209 -0
- package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
- package/src/audit/fixes/social-meta-fixes.ts +463 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/runner.test.ts +299 -0
- package/src/audit/runner.ts +130 -0
- package/src/audit/types.ts +1953 -0
- package/src/content/featured-snippet.ts +367 -0
- package/src/content/generator.test.ts +534 -0
- package/src/content/generator.ts +501 -0
- package/src/content/headline.ts +317 -0
- package/src/content/index.ts +62 -0
- package/src/content/intent.ts +258 -0
- package/src/content/keyword-density.ts +349 -0
- package/src/content/readability.ts +262 -0
- package/src/executor.ts +336 -0
- package/src/fixer.ts +416 -0
- package/src/frameworks/detector.test.ts +248 -0
- package/src/frameworks/detector.ts +371 -0
- package/src/frameworks/index.ts +68 -0
- package/src/frameworks/recipes/angular.yaml +171 -0
- package/src/frameworks/recipes/astro.yaml +206 -0
- package/src/frameworks/recipes/django.yaml +180 -0
- package/src/frameworks/recipes/laravel.yaml +137 -0
- package/src/frameworks/recipes/nextjs.yaml +268 -0
- package/src/frameworks/recipes/nuxt.yaml +175 -0
- package/src/frameworks/recipes/rails.yaml +188 -0
- package/src/frameworks/recipes/react.yaml +202 -0
- package/src/frameworks/recipes/sveltekit.yaml +154 -0
- package/src/frameworks/recipes/vue.yaml +137 -0
- package/src/frameworks/recipes/wordpress.yaml +209 -0
- package/src/frameworks/suggestion-engine.ts +320 -0
- package/src/geo/geo-content.test.ts +305 -0
- package/src/geo/geo-content.ts +266 -0
- package/src/geo/geo-history.test.ts +473 -0
- package/src/geo/geo-history.ts +433 -0
- package/src/geo/geo-tracker.test.ts +359 -0
- package/src/geo/geo-tracker.ts +411 -0
- package/src/geo/index.ts +10 -0
- package/src/git/commit-helper.test.ts +261 -0
- package/src/git/commit-helper.ts +329 -0
- package/src/git/index.ts +12 -0
- package/src/git/pr-helper.test.ts +284 -0
- package/src/git/pr-helper.ts +307 -0
- package/src/index.ts +66 -0
- package/src/keywords/ai-keyword-engine.ts +1062 -0
- package/src/keywords/ai-summarizer.ts +387 -0
- package/src/keywords/ci-mode.ts +555 -0
- package/src/keywords/engine.ts +359 -0
- package/src/keywords/index.ts +151 -0
- package/src/keywords/llm-judge.ts +357 -0
- package/src/keywords/nlp-analysis.ts +706 -0
- package/src/keywords/prioritizer.ts +295 -0
- package/src/keywords/site-crawler.ts +342 -0
- package/src/keywords/sources/autocomplete.ts +139 -0
- package/src/keywords/sources/competitive-search.ts +450 -0
- package/src/keywords/sources/competitor-analysis.ts +374 -0
- package/src/keywords/sources/dataforseo.ts +206 -0
- package/src/keywords/sources/free-sources.ts +294 -0
- package/src/keywords/sources/gsc.ts +123 -0
- package/src/keywords/topic-grouping.ts +327 -0
- package/src/keywords/types.ts +144 -0
- package/src/keywords/wizard.ts +457 -0
- package/src/loader.ts +40 -0
- package/src/reports/index.ts +7 -0
- package/src/reports/report-generator.test.ts +293 -0
- package/src/reports/report-generator.ts +713 -0
- package/src/scheduler/alerts.test.ts +458 -0
- package/src/scheduler/alerts.ts +328 -0
- package/src/scheduler/index.ts +8 -0
- package/src/scheduler/scheduled-audit.test.ts +377 -0
- package/src/scheduler/scheduled-audit.ts +149 -0
- package/src/test/integration-test.ts +325 -0
- package/src/tools/analyzer.ts +373 -0
- package/src/tools/crawl.ts +293 -0
- package/src/tools/files.ts +301 -0
- package/src/tools/h1-fixer.ts +249 -0
- package/src/tools/index.ts +67 -0
- package/src/tracking/github-action.ts +326 -0
- package/src/tracking/google-analytics.ts +265 -0
- package/src/tracking/index.ts +45 -0
- package/src/tracking/report-generator.ts +386 -0
- package/src/tracking/search-console.ts +335 -0
- package/src/types.ts +134 -0
- package/src/utils/http.ts +302 -0
- package/src/wasm-adapter.ts +297 -0
- package/src/wasm-entry.ts +14 -0
- package/tsconfig.json +17 -0
- package/tsup.wasm.config.ts +26 -0
- 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
|
+
});
|