@rankcli/agent-runtime 0.0.8 → 0.0.11
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 +90 -196
- package/dist/analyzer-GMURJADU.mjs +7 -0
- package/dist/chunk-2JADKV3Z.mjs +244 -0
- package/dist/chunk-3ZSCLNTW.mjs +557 -0
- package/dist/chunk-4E4MQOSP.mjs +374 -0
- package/dist/chunk-6BWS3CLP.mjs +16 -0
- package/dist/chunk-AK2IC22C.mjs +206 -0
- package/dist/chunk-K6VSXDD6.mjs +293 -0
- package/dist/chunk-M27NQCWW.mjs +303 -0
- package/dist/{chunk-YNZYHEYM.mjs → chunk-PJLNXOLN.mjs} +0 -14
- package/dist/chunk-VSQD74I7.mjs +474 -0
- package/dist/core-web-vitals-analyzer-TE6LQJMS.mjs +7 -0
- package/dist/geo-analyzer-D47LTMMA.mjs +25 -0
- package/dist/image-optimization-analyzer-XP4OQGRP.mjs +9 -0
- package/dist/index.d.mts +1523 -17
- package/dist/index.d.ts +1523 -17
- package/dist/index.js +9582 -2664
- package/dist/index.mjs +4812 -380
- package/dist/internal-linking-analyzer-MRMBV7NM.mjs +9 -0
- package/dist/mobile-seo-analyzer-67HNQ7IO.mjs +7 -0
- package/dist/security-headers-analyzer-3ZUQARS5.mjs +9 -0
- package/dist/structured-data-analyzer-2I4NQAUP.mjs +9 -0
- package/package.json +2 -2
- package/src/analyzers/core-web-vitals-analyzer.test.ts +236 -0
- package/src/analyzers/core-web-vitals-analyzer.ts +557 -0
- package/src/analyzers/geo-analyzer.test.ts +310 -0
- package/src/analyzers/geo-analyzer.ts +814 -0
- package/src/analyzers/image-optimization-analyzer.test.ts +145 -0
- package/src/analyzers/image-optimization-analyzer.ts +348 -0
- package/src/analyzers/index.ts +233 -0
- package/src/analyzers/internal-linking-analyzer.test.ts +141 -0
- package/src/analyzers/internal-linking-analyzer.ts +419 -0
- package/src/analyzers/mobile-seo-analyzer.test.ts +140 -0
- package/src/analyzers/mobile-seo-analyzer.ts +455 -0
- package/src/analyzers/security-headers-analyzer.test.ts +115 -0
- package/src/analyzers/security-headers-analyzer.ts +318 -0
- package/src/analyzers/structured-data-analyzer.test.ts +210 -0
- package/src/analyzers/structured-data-analyzer.ts +590 -0
- package/src/audit/engine.ts +3 -3
- package/src/audit/types.ts +3 -2
- package/src/fixer/framework-fixes.test.ts +489 -0
- package/src/fixer/framework-fixes.ts +3418 -0
- package/src/fixer/index.ts +1 -0
- package/src/fixer/schemas.ts +971 -0
- package/src/frameworks/detector.ts +642 -114
- package/src/frameworks/suggestion-engine.ts +38 -1
- package/src/index.ts +6 -0
- package/src/types.ts +15 -1
- package/dist/analyzer-2CSWIQGD.mjs +0 -6
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive tests for all framework-specific SEO fix generators
|
|
3
|
+
*
|
|
4
|
+
* Tests 25+ frameworks across JavaScript, Ruby, Python, PHP, Java, C#, Go, and Elixir
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
generateReactSEOHead,
|
|
10
|
+
generateNextJsAppRouterMetadata,
|
|
11
|
+
generateNextJsPagesRouterHead,
|
|
12
|
+
generateNuxtSEOHead,
|
|
13
|
+
generateVueSEOHead,
|
|
14
|
+
generateAstroBaseHead,
|
|
15
|
+
generateSvelteKitSEOHead,
|
|
16
|
+
generateAngularSEOService,
|
|
17
|
+
generateRailsSEOHelper,
|
|
18
|
+
generateDjangoSEOHelper,
|
|
19
|
+
generateLaravelSEOHelper,
|
|
20
|
+
generateSpringBootSEO,
|
|
21
|
+
generatePhoenixSEO,
|
|
22
|
+
generateGoSEO,
|
|
23
|
+
generateAspNetCoreSEO,
|
|
24
|
+
generateHTMXSEO,
|
|
25
|
+
generateHugoSEO,
|
|
26
|
+
generateJekyllSEO,
|
|
27
|
+
generateEleventySEO,
|
|
28
|
+
generateRemixSEO,
|
|
29
|
+
getFrameworkSpecificFixExtended,
|
|
30
|
+
type MetaFixOptions,
|
|
31
|
+
type GeneratedCode,
|
|
32
|
+
} from './framework-fixes.js';
|
|
33
|
+
import type { FrameworkInfo } from '../types.js';
|
|
34
|
+
|
|
35
|
+
const TEST_OPTIONS: MetaFixOptions = {
|
|
36
|
+
siteName: 'TestSite',
|
|
37
|
+
siteUrl: 'https://example.com',
|
|
38
|
+
title: 'Test Page',
|
|
39
|
+
description: 'A test description for SEO purposes.',
|
|
40
|
+
image: 'https://example.com/og-image.png',
|
|
41
|
+
twitterHandle: '@testsite',
|
|
42
|
+
locale: 'en_US',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Frameworks that use template variables instead of hardcoded values
|
|
46
|
+
const TEMPLATE_FRAMEWORKS = ['Hugo', 'Jekyll', 'Eleventy', 'Go'];
|
|
47
|
+
|
|
48
|
+
// Helper to validate generated code structure
|
|
49
|
+
function validateGeneratedCode(result: GeneratedCode, framework: string) {
|
|
50
|
+
// Must have a file path
|
|
51
|
+
expect(result.file, `${framework}: file path`).toBeTruthy();
|
|
52
|
+
expect(typeof result.file).toBe('string');
|
|
53
|
+
|
|
54
|
+
// Must have code content
|
|
55
|
+
expect(result.code, `${framework}: code content`).toBeTruthy();
|
|
56
|
+
expect(result.code.length, `${framework}: code length`).toBeGreaterThan(100);
|
|
57
|
+
|
|
58
|
+
// Must have explanation
|
|
59
|
+
expect(result.explanation, `${framework}: explanation`).toBeTruthy();
|
|
60
|
+
|
|
61
|
+
// Template-based frameworks store site name in config files, not templates
|
|
62
|
+
if (TEMPLATE_FRAMEWORKS.includes(framework)) {
|
|
63
|
+
// Check if site name is in additional files (config.toml, _config.yml, metadata.json)
|
|
64
|
+
const allCode = result.code + (result.additionalFiles?.map(f => f.code).join('') || '');
|
|
65
|
+
expect(allCode, `${framework}: contains site name or URL somewhere`).toMatch(/TestSite|example\.com/);
|
|
66
|
+
} else {
|
|
67
|
+
// Code should contain site name
|
|
68
|
+
expect(result.code, `${framework}: contains site name`).toContain('TestSite');
|
|
69
|
+
// Code should contain site URL
|
|
70
|
+
expect(result.code, `${framework}: contains site URL`).toContain('example.com');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Helper to validate SEO features in generated code
|
|
75
|
+
function validateSEOFeatures(code: string, framework: string, additionalFiles?: { code: string }[]) {
|
|
76
|
+
// Combine main code with additional files for template frameworks
|
|
77
|
+
const allCode = code + (additionalFiles?.map(f => f.code).join('') || '');
|
|
78
|
+
|
|
79
|
+
// Should have Open Graph tags or references
|
|
80
|
+
const hasOG = allCode.includes('og:') ||
|
|
81
|
+
allCode.includes('openGraph') ||
|
|
82
|
+
allCode.includes('og_') ||
|
|
83
|
+
allCode.includes('Open Graph') ||
|
|
84
|
+
allCode.includes('property="og') ||
|
|
85
|
+
allCode.includes("property='og") ||
|
|
86
|
+
allCode.includes('og:type') ||
|
|
87
|
+
allCode.includes('OG');
|
|
88
|
+
expect(hasOG, `${framework}: has Open Graph`).toBe(true);
|
|
89
|
+
|
|
90
|
+
// Should have Twitter Card tags or references
|
|
91
|
+
const hasTwitter = allCode.includes('twitter:') ||
|
|
92
|
+
allCode.includes('twitter') ||
|
|
93
|
+
allCode.includes('Twitter');
|
|
94
|
+
expect(hasTwitter, `${framework}: has Twitter`).toBe(true);
|
|
95
|
+
|
|
96
|
+
// Should have JSON-LD or schema references
|
|
97
|
+
const hasSchema = allCode.includes('schema.org') ||
|
|
98
|
+
allCode.includes('json-ld') ||
|
|
99
|
+
allCode.includes('JSON-LD') ||
|
|
100
|
+
allCode.includes('application/ld+json') ||
|
|
101
|
+
allCode.includes('jsonLd') ||
|
|
102
|
+
allCode.includes('json_ld') ||
|
|
103
|
+
allCode.includes('Schema');
|
|
104
|
+
expect(hasSchema, `${framework}: has JSON-LD schema`).toBe(true);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe('Framework SEO Fix Generators', () => {
|
|
108
|
+
describe('JavaScript/TypeScript Frameworks', () => {
|
|
109
|
+
it('React (react-helmet-async)', () => {
|
|
110
|
+
const result = generateReactSEOHead(TEST_OPTIONS);
|
|
111
|
+
validateGeneratedCode(result, 'React');
|
|
112
|
+
validateSEOFeatures(result.code, 'React');
|
|
113
|
+
|
|
114
|
+
expect(result.code).toContain('Helmet');
|
|
115
|
+
expect(result.code).toContain('react-helmet-async');
|
|
116
|
+
expect(result.installCommands).toContain('npm install react-helmet-async');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('Next.js App Router (metadata export)', () => {
|
|
120
|
+
const result = generateNextJsAppRouterMetadata(TEST_OPTIONS);
|
|
121
|
+
validateGeneratedCode(result, 'Next.js App Router');
|
|
122
|
+
validateSEOFeatures(result.code, 'Next.js App Router');
|
|
123
|
+
|
|
124
|
+
expect(result.code).toContain('Metadata');
|
|
125
|
+
expect(result.code).toContain('metadataBase');
|
|
126
|
+
expect(result.additionalFiles?.length).toBeGreaterThan(0);
|
|
127
|
+
|
|
128
|
+
// Should have robots.ts
|
|
129
|
+
const robotsFile = result.additionalFiles?.find(f => f.file.includes('robots'));
|
|
130
|
+
expect(robotsFile).toBeTruthy();
|
|
131
|
+
|
|
132
|
+
// Should have sitemap.ts
|
|
133
|
+
const sitemapFile = result.additionalFiles?.find(f => f.file.includes('sitemap'));
|
|
134
|
+
expect(sitemapFile).toBeTruthy();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('Next.js Pages Router (Head component)', () => {
|
|
138
|
+
const result = generateNextJsPagesRouterHead(TEST_OPTIONS);
|
|
139
|
+
validateGeneratedCode(result, 'Next.js Pages Router');
|
|
140
|
+
validateSEOFeatures(result.code, 'Next.js Pages Router');
|
|
141
|
+
|
|
142
|
+
expect(result.code).toContain('next/head');
|
|
143
|
+
expect(result.code).toContain('<Head>');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('Nuxt (useHead composable)', () => {
|
|
147
|
+
const result = generateNuxtSEOHead(TEST_OPTIONS);
|
|
148
|
+
validateGeneratedCode(result, 'Nuxt');
|
|
149
|
+
validateSEOFeatures(result.code, 'Nuxt');
|
|
150
|
+
|
|
151
|
+
expect(result.code).toContain('useHead');
|
|
152
|
+
expect(result.code).toContain('useSEO');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('Vue.js (@unhead/vue)', () => {
|
|
156
|
+
const result = generateVueSEOHead(TEST_OPTIONS);
|
|
157
|
+
validateGeneratedCode(result, 'Vue.js');
|
|
158
|
+
validateSEOFeatures(result.code, 'Vue.js');
|
|
159
|
+
|
|
160
|
+
expect(result.code).toContain('@unhead/vue');
|
|
161
|
+
expect(result.installCommands).toContain('npm install @unhead/vue');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('Astro (BaseHead component)', () => {
|
|
165
|
+
const result = generateAstroBaseHead(TEST_OPTIONS);
|
|
166
|
+
validateGeneratedCode(result, 'Astro');
|
|
167
|
+
validateSEOFeatures(result.code, 'Astro');
|
|
168
|
+
|
|
169
|
+
expect(result.file).toContain('.astro');
|
|
170
|
+
expect(result.code).toContain('Astro.props');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('SvelteKit (svelte:head)', () => {
|
|
174
|
+
const result = generateSvelteKitSEOHead(TEST_OPTIONS);
|
|
175
|
+
validateGeneratedCode(result, 'SvelteKit');
|
|
176
|
+
validateSEOFeatures(result.code, 'SvelteKit');
|
|
177
|
+
|
|
178
|
+
expect(result.code).toContain('svelte:head');
|
|
179
|
+
expect(result.code).toContain('$app/stores');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('Angular (SEO Service)', () => {
|
|
183
|
+
const result = generateAngularSEOService(TEST_OPTIONS);
|
|
184
|
+
validateGeneratedCode(result, 'Angular');
|
|
185
|
+
validateSEOFeatures(result.code, 'Angular');
|
|
186
|
+
|
|
187
|
+
expect(result.code).toContain('@Injectable');
|
|
188
|
+
expect(result.code).toContain('Meta');
|
|
189
|
+
expect(result.code).toContain('Title');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('Remix (meta function)', () => {
|
|
193
|
+
const result = generateRemixSEO(TEST_OPTIONS);
|
|
194
|
+
validateGeneratedCode(result, 'Remix');
|
|
195
|
+
validateSEOFeatures(result.code, 'Remix');
|
|
196
|
+
|
|
197
|
+
expect(result.code).toContain('V2_MetaFunction');
|
|
198
|
+
expect(result.code).toContain('generateMeta');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('Backend Frameworks', () => {
|
|
203
|
+
it('Ruby on Rails (meta-tags gem)', () => {
|
|
204
|
+
const result = generateRailsSEOHelper(TEST_OPTIONS);
|
|
205
|
+
validateGeneratedCode(result, 'Rails');
|
|
206
|
+
validateSEOFeatures(result.code, 'Rails');
|
|
207
|
+
|
|
208
|
+
expect(result.file).toContain('.rb');
|
|
209
|
+
expect(result.code).toContain('module SeoHelper');
|
|
210
|
+
expect(result.code).toContain('set_meta_tags');
|
|
211
|
+
expect(result.code).toContain('json_ld_tags');
|
|
212
|
+
expect(result.installCommands).toContain('bundle add meta-tags');
|
|
213
|
+
|
|
214
|
+
// Should have layout file
|
|
215
|
+
const layoutFile = result.additionalFiles?.find(f => f.file.includes('application.html.erb'));
|
|
216
|
+
expect(layoutFile).toBeTruthy();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('Django (django-meta)', () => {
|
|
220
|
+
const result = generateDjangoSEOHelper(TEST_OPTIONS);
|
|
221
|
+
validateGeneratedCode(result, 'Django');
|
|
222
|
+
validateSEOFeatures(result.code, 'Django');
|
|
223
|
+
|
|
224
|
+
expect(result.file).toContain('.py');
|
|
225
|
+
expect(result.code).toContain('class SEOMeta');
|
|
226
|
+
expect(result.code).toContain('SEOContextMixin');
|
|
227
|
+
expect(result.code).toContain('def article_schema');
|
|
228
|
+
expect(result.installCommands).toContain('pip install django-meta');
|
|
229
|
+
|
|
230
|
+
// Should have base.html template
|
|
231
|
+
const templateFile = result.additionalFiles?.find(f => f.file.includes('base.html'));
|
|
232
|
+
expect(templateFile).toBeTruthy();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('Laravel (Blade + SEOService)', () => {
|
|
236
|
+
const result = generateLaravelSEOHelper(TEST_OPTIONS);
|
|
237
|
+
validateGeneratedCode(result, 'Laravel');
|
|
238
|
+
validateSEOFeatures(result.code, 'Laravel');
|
|
239
|
+
|
|
240
|
+
expect(result.file).toContain('.php');
|
|
241
|
+
expect(result.code).toContain('class SEOService');
|
|
242
|
+
expect(result.code).toContain('namespace App\\Services');
|
|
243
|
+
expect(result.code).toContain('public function setMeta');
|
|
244
|
+
|
|
245
|
+
// Should have Blade component
|
|
246
|
+
const bladeFile = result.additionalFiles?.find(f => f.file.includes('.blade.php'));
|
|
247
|
+
expect(bladeFile).toBeTruthy();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('Spring Boot (Thymeleaf)', () => {
|
|
251
|
+
const result = generateSpringBootSEO(TEST_OPTIONS);
|
|
252
|
+
validateGeneratedCode(result, 'Spring Boot');
|
|
253
|
+
validateSEOFeatures(result.code, 'Spring Boot');
|
|
254
|
+
|
|
255
|
+
expect(result.file).toContain('.java');
|
|
256
|
+
expect(result.code).toContain('public class SEOService');
|
|
257
|
+
expect(result.code).toContain('@Service');
|
|
258
|
+
expect(result.code).toContain('Map<String, Object>');
|
|
259
|
+
|
|
260
|
+
// Should have Thymeleaf fragment
|
|
261
|
+
const thymeleafFile = result.additionalFiles?.find(f => f.file.includes('.html'));
|
|
262
|
+
expect(thymeleafFile).toBeTruthy();
|
|
263
|
+
expect(thymeleafFile?.code).toContain('th:');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('Phoenix/Elixir (HEEx)', () => {
|
|
267
|
+
const result = generatePhoenixSEO(TEST_OPTIONS);
|
|
268
|
+
validateGeneratedCode(result, 'Phoenix');
|
|
269
|
+
validateSEOFeatures(result.code, 'Phoenix');
|
|
270
|
+
|
|
271
|
+
expect(result.file).toContain('.ex');
|
|
272
|
+
expect(result.code).toContain('defmodule');
|
|
273
|
+
expect(result.code).toContain('use Phoenix.Component');
|
|
274
|
+
expect(result.code).toContain('def seo_meta');
|
|
275
|
+
expect(result.code).toContain('~H"""');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('Go (Gin/Echo/Fiber templates)', () => {
|
|
279
|
+
const result = generateGoSEO(TEST_OPTIONS);
|
|
280
|
+
validateGeneratedCode(result, 'Go');
|
|
281
|
+
validateSEOFeatures(result.code, 'Go', result.additionalFiles);
|
|
282
|
+
|
|
283
|
+
expect(result.file).toContain('.go');
|
|
284
|
+
expect(result.code).toContain('package seo');
|
|
285
|
+
expect(result.code).toContain('type Meta struct');
|
|
286
|
+
expect(result.code).toContain('func WebsiteSchema');
|
|
287
|
+
|
|
288
|
+
// Should have Go template
|
|
289
|
+
const templateFile = result.additionalFiles?.find(f => f.file.includes('.html'));
|
|
290
|
+
expect(templateFile).toBeTruthy();
|
|
291
|
+
expect(templateFile?.code).toContain('{{');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('ASP.NET Core (Razor)', () => {
|
|
295
|
+
const result = generateAspNetCoreSEO(TEST_OPTIONS);
|
|
296
|
+
validateGeneratedCode(result, 'ASP.NET Core');
|
|
297
|
+
validateSEOFeatures(result.code, 'ASP.NET Core');
|
|
298
|
+
|
|
299
|
+
expect(result.file).toContain('.cs');
|
|
300
|
+
expect(result.code).toContain('public class SEOService');
|
|
301
|
+
expect(result.code).toContain('namespace');
|
|
302
|
+
expect(result.code).toContain('public class SEOMeta');
|
|
303
|
+
|
|
304
|
+
// Should have Razor partial
|
|
305
|
+
const razorFile = result.additionalFiles?.find(f => f.file.includes('.cshtml'));
|
|
306
|
+
expect(razorFile).toBeTruthy();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('Hypermedia Frameworks', () => {
|
|
311
|
+
it('HTMX (server-rendered)', () => {
|
|
312
|
+
const result = generateHTMXSEO(TEST_OPTIONS);
|
|
313
|
+
validateGeneratedCode(result, 'HTMX');
|
|
314
|
+
validateSEOFeatures(result.code, 'HTMX');
|
|
315
|
+
|
|
316
|
+
expect(result.code).toContain('HTMX');
|
|
317
|
+
expect(result.code).toContain('server-rendered');
|
|
318
|
+
|
|
319
|
+
// Should have base template
|
|
320
|
+
const baseFile = result.additionalFiles?.find(f => f.file.includes('base.html'));
|
|
321
|
+
expect(baseFile).toBeTruthy();
|
|
322
|
+
expect(baseFile?.code).toContain('hx-boost');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('Static Site Generators', () => {
|
|
327
|
+
it('Hugo (Go templates)', () => {
|
|
328
|
+
const result = generateHugoSEO(TEST_OPTIONS);
|
|
329
|
+
validateGeneratedCode(result, 'Hugo');
|
|
330
|
+
validateSEOFeatures(result.code, 'Hugo', result.additionalFiles);
|
|
331
|
+
|
|
332
|
+
expect(result.file).toContain('partials');
|
|
333
|
+
expect(result.code).toContain('{{-');
|
|
334
|
+
expect(result.code).toContain('.Title');
|
|
335
|
+
expect(result.code).toContain('site.Title');
|
|
336
|
+
|
|
337
|
+
// Should have config.toml
|
|
338
|
+
const configFile = result.additionalFiles?.find(f => f.file.includes('config.toml'));
|
|
339
|
+
expect(configFile).toBeTruthy();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('Jekyll (Liquid templates)', () => {
|
|
343
|
+
const result = generateJekyllSEO(TEST_OPTIONS);
|
|
344
|
+
validateGeneratedCode(result, 'Jekyll');
|
|
345
|
+
validateSEOFeatures(result.code, 'Jekyll', result.additionalFiles);
|
|
346
|
+
|
|
347
|
+
expect(result.file).toContain('_includes');
|
|
348
|
+
expect(result.code).toContain('{%');
|
|
349
|
+
expect(result.code).toContain('page.title');
|
|
350
|
+
expect(result.code).toContain('site.title');
|
|
351
|
+
|
|
352
|
+
// Should have _config.yml
|
|
353
|
+
const configFile = result.additionalFiles?.find(f => f.file.includes('_config.yml'));
|
|
354
|
+
expect(configFile).toBeTruthy();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('Eleventy (Nunjucks templates)', () => {
|
|
358
|
+
const result = generateEleventySEO(TEST_OPTIONS);
|
|
359
|
+
validateGeneratedCode(result, 'Eleventy');
|
|
360
|
+
validateSEOFeatures(result.code, 'Eleventy', result.additionalFiles);
|
|
361
|
+
|
|
362
|
+
expect(result.file).toContain('.njk');
|
|
363
|
+
expect(result.code).toContain('{%');
|
|
364
|
+
expect(result.code).toContain('metadata');
|
|
365
|
+
|
|
366
|
+
// Should have metadata.json
|
|
367
|
+
const metadataFile = result.additionalFiles?.find(f => f.file.includes('metadata.json'));
|
|
368
|
+
expect(metadataFile).toBeTruthy();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe('Framework Detection Integration', () => {
|
|
373
|
+
const frameworks: Array<{ name: string; info: FrameworkInfo }> = [
|
|
374
|
+
{ name: 'React', info: { name: 'react', metaPattern: 'head-component' } },
|
|
375
|
+
{ name: 'Next.js App', info: { name: 'nextjs', router: 'app', metaPattern: 'metadata-export' } },
|
|
376
|
+
{ name: 'Next.js Pages', info: { name: 'nextjs', router: 'pages', metaPattern: 'head-component' } },
|
|
377
|
+
{ name: 'Vue', info: { name: 'vue', metaPattern: 'head-component' } },
|
|
378
|
+
{ name: 'Nuxt', info: { name: 'nuxt', metaPattern: 'head-component' } },
|
|
379
|
+
{ name: 'Angular', info: { name: 'angular', metaPattern: 'head-component' } },
|
|
380
|
+
{ name: 'Svelte', info: { name: 'svelte', metaPattern: 'head-component' } },
|
|
381
|
+
{ name: 'SvelteKit', info: { name: 'sveltekit', metaPattern: 'head-component' } },
|
|
382
|
+
{ name: 'Astro', info: { name: 'astro', metaPattern: 'frontmatter' } },
|
|
383
|
+
{ name: 'Remix', info: { name: 'remix', metaPattern: 'meta-function' } },
|
|
384
|
+
{ name: 'Rails', info: { name: 'rails', language: 'ruby', metaPattern: 'erb-helpers' } },
|
|
385
|
+
{ name: 'Django', info: { name: 'django', language: 'python', metaPattern: 'django-templates' } },
|
|
386
|
+
{ name: 'Laravel', info: { name: 'laravel', language: 'php', metaPattern: 'blade-components' } },
|
|
387
|
+
{ name: 'Spring', info: { name: 'spring', language: 'java', metaPattern: 'thymeleaf' } },
|
|
388
|
+
{ name: 'ASP.NET', info: { name: 'aspnet', language: 'csharp', metaPattern: 'razor' } },
|
|
389
|
+
{ name: 'Phoenix', info: { name: 'phoenix', language: 'elixir', metaPattern: 'heex' } },
|
|
390
|
+
{ name: 'Go', info: { name: 'go', language: 'go', metaPattern: 'go-templates' } },
|
|
391
|
+
{ name: 'HTMX', info: { name: 'htmx', metaPattern: 'server-rendered' } },
|
|
392
|
+
{ name: 'Hugo', info: { name: 'hugo', metaPattern: 'frontmatter' } },
|
|
393
|
+
{ name: 'Jekyll', info: { name: 'jekyll', metaPattern: 'frontmatter' } },
|
|
394
|
+
{ name: 'Eleventy', info: { name: 'eleventy', metaPattern: 'frontmatter' } },
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
for (const { name, info } of frameworks) {
|
|
398
|
+
it(`getFrameworkSpecificFixExtended returns valid code for ${name}`, () => {
|
|
399
|
+
const result = getFrameworkSpecificFixExtended(info, TEST_OPTIONS);
|
|
400
|
+
validateGeneratedCode(result, name);
|
|
401
|
+
validateSEOFeatures(result.code, name, result.additionalFiles);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('Generated Code Quality', () => {
|
|
407
|
+
it('all generators include robots.txt', () => {
|
|
408
|
+
const generators = [
|
|
409
|
+
generateNextJsAppRouterMetadata,
|
|
410
|
+
generateNuxtSEOHead,
|
|
411
|
+
generateRailsSEOHelper,
|
|
412
|
+
generateDjangoSEOHelper,
|
|
413
|
+
generateLaravelSEOHelper,
|
|
414
|
+
generateSpringBootSEO,
|
|
415
|
+
generatePhoenixSEO,
|
|
416
|
+
generateGoSEO,
|
|
417
|
+
generateAspNetCoreSEO,
|
|
418
|
+
generateHTMXSEO,
|
|
419
|
+
generateHugoSEO,
|
|
420
|
+
generateJekyllSEO,
|
|
421
|
+
generateEleventySEO,
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
for (const generator of generators) {
|
|
425
|
+
const result = generator(TEST_OPTIONS);
|
|
426
|
+
const hasRobots = result.additionalFiles?.some(f =>
|
|
427
|
+
f.file.includes('robots') || f.code.includes('User-agent')
|
|
428
|
+
);
|
|
429
|
+
expect(hasRobots, `${generator.name}: has robots.txt`).toBe(true);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('all generators include AI crawler support (GPTBot)', () => {
|
|
434
|
+
const generators = [
|
|
435
|
+
generateNextJsAppRouterMetadata,
|
|
436
|
+
generateNuxtSEOHead,
|
|
437
|
+
generateRailsSEOHelper,
|
|
438
|
+
generateDjangoSEOHelper,
|
|
439
|
+
generateLaravelSEOHelper,
|
|
440
|
+
generateSpringBootSEO,
|
|
441
|
+
generatePhoenixSEO,
|
|
442
|
+
generateGoSEO,
|
|
443
|
+
generateAspNetCoreSEO,
|
|
444
|
+
generateHTMXSEO,
|
|
445
|
+
generateHugoSEO,
|
|
446
|
+
generateJekyllSEO,
|
|
447
|
+
generateEleventySEO,
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
for (const generator of generators) {
|
|
451
|
+
const result = generator(TEST_OPTIONS);
|
|
452
|
+
const robotsFile = result.additionalFiles?.find(f =>
|
|
453
|
+
f.file.includes('robots') || f.code.includes('User-agent')
|
|
454
|
+
);
|
|
455
|
+
expect(robotsFile?.code, `${generator.name}: GPTBot support`).toContain('GPTBot');
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('all generators produce valid schema generators', () => {
|
|
460
|
+
const generators = [
|
|
461
|
+
generateReactSEOHead,
|
|
462
|
+
generateNextJsAppRouterMetadata,
|
|
463
|
+
generateNuxtSEOHead,
|
|
464
|
+
generateAngularSEOService,
|
|
465
|
+
generateRailsSEOHelper,
|
|
466
|
+
generateDjangoSEOHelper,
|
|
467
|
+
generateLaravelSEOHelper,
|
|
468
|
+
generateSpringBootSEO,
|
|
469
|
+
generatePhoenixSEO,
|
|
470
|
+
generateGoSEO,
|
|
471
|
+
generateAspNetCoreSEO,
|
|
472
|
+
generateRemixSEO,
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
for (const generator of generators) {
|
|
476
|
+
const result = generator(TEST_OPTIONS);
|
|
477
|
+
const allCode = result.code + (result.additionalFiles?.map(f => f.code).join('') || '');
|
|
478
|
+
|
|
479
|
+
// Should have article schema generator
|
|
480
|
+
expect(allCode, `${generator.name}: article schema`).toMatch(/article|Article/);
|
|
481
|
+
|
|
482
|
+
// Should have product schema generator (most frameworks)
|
|
483
|
+
if (!['generateHTMXSEO'].includes(generator.name)) {
|
|
484
|
+
expect(allCode, `${generator.name}: product schema`).toMatch(/product|Product/i);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
});
|