@rankcli/agent-runtime 0.0.6 → 0.0.8

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.
@@ -0,0 +1,1869 @@
1
+ /**
2
+ * Framework-Specific SEO Fix Generators
3
+ *
4
+ * Industry-leading SEO implementations for each framework including:
5
+ * - Dynamic meta tags with full OG/Twitter support
6
+ * - JSON-LD structured data
7
+ * - Robots.txt and sitemap generation
8
+ * - Performance optimizations (preconnect, preload)
9
+ * - Canonical URL handling
10
+ * - Internationalization support
11
+ */
12
+
13
+ import type { FrameworkInfo } from '../types.js';
14
+
15
+ export interface MetaFixOptions {
16
+ siteName: string;
17
+ siteUrl: string;
18
+ title?: string;
19
+ description?: string;
20
+ image?: string;
21
+ twitterHandle?: string;
22
+ locale?: string;
23
+ }
24
+
25
+ export interface GeneratedCode {
26
+ file: string;
27
+ code: string;
28
+ explanation: string;
29
+ installCommands?: string[];
30
+ imports?: string[];
31
+ additionalFiles?: { file: string; code: string; explanation: string }[];
32
+ }
33
+
34
+ // ============================================================================
35
+ // REACT (Vite/CRA) - Comprehensive SEO with react-helmet-async
36
+ // ============================================================================
37
+
38
+ export function generateReactSEOHead(options: MetaFixOptions): GeneratedCode {
39
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
40
+
41
+ return {
42
+ file: 'src/components/SEOHead.tsx',
43
+ code: `import { Helmet } from 'react-helmet-async';
44
+
45
+ /**
46
+ * SEO Head Component
47
+ *
48
+ * Comprehensive SEO meta tags following best practices:
49
+ * - Primary meta tags (title, description)
50
+ * - Open Graph for Facebook/LinkedIn
51
+ * - Twitter Card for X/Twitter
52
+ * - JSON-LD structured data
53
+ * - Canonical URLs
54
+ *
55
+ * @example
56
+ * <SEOHead
57
+ * title="Product Name"
58
+ * description="Product description"
59
+ * type="product"
60
+ * schema={{
61
+ * "@type": "Product",
62
+ * name: "Product Name",
63
+ * price: "99.00"
64
+ * }}
65
+ * />
66
+ */
67
+
68
+ interface SEOHeadProps {
69
+ // Required
70
+ title?: string;
71
+ description?: string;
72
+
73
+ // URLs
74
+ url?: string;
75
+ canonical?: string;
76
+ image?: string;
77
+
78
+ // Page type
79
+ type?: 'website' | 'article' | 'product' | 'profile';
80
+
81
+ // Article-specific
82
+ publishedTime?: string;
83
+ modifiedTime?: string;
84
+ author?: string;
85
+ section?: string;
86
+ tags?: string[];
87
+
88
+ // Twitter
89
+ twitterCard?: 'summary' | 'summary_large_image' | 'player';
90
+
91
+ // Structured data
92
+ schema?: Record<string, unknown> | Record<string, unknown>[];
93
+
94
+ // Robots
95
+ noindex?: boolean;
96
+ nofollow?: boolean;
97
+
98
+ // Alternate languages
99
+ alternates?: { hrefLang: string; href: string }[];
100
+ }
101
+
102
+ const SITE_NAME = '${siteName}';
103
+ const SITE_URL = '${siteUrl}';
104
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
105
+ const TWITTER_HANDLE = '${twitterHandle || ''}';
106
+ const DEFAULT_LOCALE = '${locale || 'en_US'}';
107
+
108
+ export function SEOHead({
109
+ title,
110
+ description = '${description || `${siteName} - A compelling description of your product or service.`}',
111
+ url,
112
+ canonical,
113
+ image = DEFAULT_IMAGE,
114
+ type = 'website',
115
+ publishedTime,
116
+ modifiedTime,
117
+ author,
118
+ section,
119
+ tags,
120
+ twitterCard = 'summary_large_image',
121
+ schema,
122
+ noindex = false,
123
+ nofollow = false,
124
+ alternates,
125
+ }: SEOHeadProps) {
126
+ const pageUrl = url || (typeof window !== 'undefined' ? window.location.href : SITE_URL);
127
+ const canonicalUrl = canonical || pageUrl;
128
+ const fullTitle = title
129
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
130
+ : SITE_NAME;
131
+
132
+ // Ensure image is absolute URL
133
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
134
+
135
+ // Build robots directive
136
+ const robotsContent = [
137
+ noindex ? 'noindex' : 'index',
138
+ nofollow ? 'nofollow' : 'follow',
139
+ ].join(', ');
140
+
141
+ // Default Organization schema
142
+ const defaultSchema = {
143
+ '@context': 'https://schema.org',
144
+ '@type': 'WebSite',
145
+ name: SITE_NAME,
146
+ url: SITE_URL,
147
+ };
148
+
149
+ // Merge with provided schema
150
+ const jsonLd = schema
151
+ ? Array.isArray(schema)
152
+ ? [defaultSchema, ...schema]
153
+ : [defaultSchema, schema]
154
+ : [defaultSchema];
155
+
156
+ return (
157
+ <Helmet>
158
+ {/* Primary Meta Tags */}
159
+ <title>{fullTitle}</title>
160
+ <meta name="title" content={fullTitle} />
161
+ <meta name="description" content={description} />
162
+ <meta name="robots" content={robotsContent} />
163
+ <link rel="canonical" href={canonicalUrl} />
164
+
165
+ {/* Open Graph / Facebook */}
166
+ <meta property="og:type" content={type} />
167
+ <meta property="og:url" content={pageUrl} />
168
+ <meta property="og:title" content={fullTitle} />
169
+ <meta property="og:description" content={description} />
170
+ <meta property="og:image" content={imageUrl} />
171
+ <meta property="og:image:width" content="1200" />
172
+ <meta property="og:image:height" content="630" />
173
+ <meta property="og:image:alt" content={fullTitle} />
174
+ <meta property="og:site_name" content={SITE_NAME} />
175
+ <meta property="og:locale" content={DEFAULT_LOCALE} />
176
+
177
+ {/* Article-specific Open Graph */}
178
+ {type === 'article' && publishedTime && (
179
+ <meta property="article:published_time" content={publishedTime} />
180
+ )}
181
+ {type === 'article' && modifiedTime && (
182
+ <meta property="article:modified_time" content={modifiedTime} />
183
+ )}
184
+ {type === 'article' && author && (
185
+ <meta property="article:author" content={author} />
186
+ )}
187
+ {type === 'article' && section && (
188
+ <meta property="article:section" content={section} />
189
+ )}
190
+ {type === 'article' && tags?.map((tag, i) => (
191
+ <meta key={i} property="article:tag" content={tag} />
192
+ ))}
193
+
194
+ {/* Twitter */}
195
+ <meta name="twitter:card" content={twitterCard} />
196
+ <meta name="twitter:url" content={pageUrl} />
197
+ <meta name="twitter:title" content={fullTitle} />
198
+ <meta name="twitter:description" content={description} />
199
+ <meta name="twitter:image" content={imageUrl} />
200
+ <meta name="twitter:image:alt" content={fullTitle} />
201
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
202
+ {TWITTER_HANDLE && <meta name="twitter:creator" content={TWITTER_HANDLE} />}
203
+
204
+ {/* Alternate Languages */}
205
+ {alternates?.map((alt, i) => (
206
+ <link key={i} rel="alternate" hrefLang={alt.hrefLang} href={alt.href} />
207
+ ))}
208
+
209
+ {/* JSON-LD Structured Data */}
210
+ <script type="application/ld+json">
211
+ {JSON.stringify(jsonLd)}
212
+ </script>
213
+ </Helmet>
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Pre-built schema generators for common page types
219
+ */
220
+ export const SchemaGenerators = {
221
+ organization: (data: {
222
+ name: string;
223
+ url: string;
224
+ logo?: string;
225
+ sameAs?: string[];
226
+ }) => ({
227
+ '@context': 'https://schema.org',
228
+ '@type': 'Organization',
229
+ name: data.name,
230
+ url: data.url,
231
+ logo: data.logo,
232
+ sameAs: data.sameAs,
233
+ }),
234
+
235
+ article: (data: {
236
+ headline: string;
237
+ description: string;
238
+ image: string;
239
+ datePublished: string;
240
+ dateModified?: string;
241
+ author: { name: string; url?: string };
242
+ }) => ({
243
+ '@context': 'https://schema.org',
244
+ '@type': 'Article',
245
+ headline: data.headline,
246
+ description: data.description,
247
+ image: data.image,
248
+ datePublished: data.datePublished,
249
+ dateModified: data.dateModified || data.datePublished,
250
+ author: {
251
+ '@type': 'Person',
252
+ name: data.author.name,
253
+ url: data.author.url,
254
+ },
255
+ }),
256
+
257
+ product: (data: {
258
+ name: string;
259
+ description: string;
260
+ image: string;
261
+ price: string;
262
+ currency?: string;
263
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
264
+ brand?: string;
265
+ sku?: string;
266
+ rating?: { value: number; count: number };
267
+ }) => ({
268
+ '@context': 'https://schema.org',
269
+ '@type': 'Product',
270
+ name: data.name,
271
+ description: data.description,
272
+ image: data.image,
273
+ brand: data.brand ? { '@type': 'Brand', name: data.brand } : undefined,
274
+ sku: data.sku,
275
+ offers: {
276
+ '@type': 'Offer',
277
+ price: data.price,
278
+ priceCurrency: data.currency || 'USD',
279
+ availability: \`https://schema.org/\${data.availability || 'InStock'}\`,
280
+ },
281
+ aggregateRating: data.rating ? {
282
+ '@type': 'AggregateRating',
283
+ ratingValue: data.rating.value,
284
+ reviewCount: data.rating.count,
285
+ } : undefined,
286
+ }),
287
+
288
+ faq: (items: { question: string; answer: string }[]) => ({
289
+ '@context': 'https://schema.org',
290
+ '@type': 'FAQPage',
291
+ mainEntity: items.map(item => ({
292
+ '@type': 'Question',
293
+ name: item.question,
294
+ acceptedAnswer: {
295
+ '@type': 'Answer',
296
+ text: item.answer,
297
+ },
298
+ })),
299
+ }),
300
+
301
+ breadcrumb: (items: { name: string; url: string }[]) => ({
302
+ '@context': 'https://schema.org',
303
+ '@type': 'BreadcrumbList',
304
+ itemListElement: items.map((item, index) => ({
305
+ '@type': 'ListItem',
306
+ position: index + 1,
307
+ name: item.name,
308
+ item: item.url,
309
+ })),
310
+ }),
311
+
312
+ localBusiness: (data: {
313
+ name: string;
314
+ description: string;
315
+ url: string;
316
+ phone: string;
317
+ address: {
318
+ street: string;
319
+ city: string;
320
+ state: string;
321
+ zip: string;
322
+ country: string;
323
+ };
324
+ geo?: { lat: number; lng: number };
325
+ hours?: string[];
326
+ priceRange?: string;
327
+ }) => ({
328
+ '@context': 'https://schema.org',
329
+ '@type': 'LocalBusiness',
330
+ name: data.name,
331
+ description: data.description,
332
+ url: data.url,
333
+ telephone: data.phone,
334
+ address: {
335
+ '@type': 'PostalAddress',
336
+ streetAddress: data.address.street,
337
+ addressLocality: data.address.city,
338
+ addressRegion: data.address.state,
339
+ postalCode: data.address.zip,
340
+ addressCountry: data.address.country,
341
+ },
342
+ geo: data.geo ? {
343
+ '@type': 'GeoCoordinates',
344
+ latitude: data.geo.lat,
345
+ longitude: data.geo.lng,
346
+ } : undefined,
347
+ openingHours: data.hours,
348
+ priceRange: data.priceRange,
349
+ }),
350
+ };`,
351
+ explanation: `Comprehensive React SEO component with:
352
+ • Full Open Graph support (including article metadata)
353
+ • Twitter Cards with all variants
354
+ • JSON-LD structured data with pre-built schema generators
355
+ • Robots directives (noindex/nofollow)
356
+ • Hreflang for internationalization
357
+ • Canonical URL handling
358
+
359
+ Install: npm install react-helmet-async
360
+ Wrap app: <HelmetProvider><App /></HelmetProvider>`,
361
+ installCommands: ['npm install react-helmet-async'],
362
+ additionalFiles: [
363
+ {
364
+ file: 'src/main.tsx',
365
+ code: `import React from 'react';
366
+ import ReactDOM from 'react-dom/client';
367
+ import { HelmetProvider } from 'react-helmet-async';
368
+ import App from './App';
369
+ import './index.css';
370
+
371
+ ReactDOM.createRoot(document.getElementById('root')!).render(
372
+ <React.StrictMode>
373
+ <HelmetProvider>
374
+ <App />
375
+ </HelmetProvider>
376
+ </React.StrictMode>,
377
+ );`,
378
+ explanation: 'Updated main.tsx with HelmetProvider wrapper.',
379
+ },
380
+ ],
381
+ };
382
+ }
383
+
384
+ // ============================================================================
385
+ // NEXT.JS (App Router) - Server-side metadata with full features
386
+ // ============================================================================
387
+
388
+ export function generateNextJsAppRouterMetadata(options: MetaFixOptions): GeneratedCode {
389
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
390
+
391
+ return {
392
+ file: 'app/layout.tsx',
393
+ code: `import type { Metadata, Viewport } from 'next';
394
+ import { Inter } from 'next/font/google';
395
+ import './globals.css';
396
+
397
+ const inter = Inter({ subsets: ['latin'], display: 'swap' });
398
+
399
+ /**
400
+ * Default metadata for all pages
401
+ * Individual pages can override with their own metadata export
402
+ */
403
+ export const metadata: Metadata = {
404
+ metadataBase: new URL('${siteUrl}'),
405
+
406
+ // Default title with template
407
+ title: {
408
+ default: '${title || siteName}',
409
+ template: \`%s | ${siteName}\`,
410
+ },
411
+
412
+ description: '${description || `${siteName} - A compelling description of your product or service.`}',
413
+
414
+ // Indexing
415
+ robots: {
416
+ index: true,
417
+ follow: true,
418
+ googleBot: {
419
+ index: true,
420
+ follow: true,
421
+ 'max-video-preview': -1,
422
+ 'max-image-preview': 'large',
423
+ 'max-snippet': -1,
424
+ },
425
+ },
426
+
427
+ // Icons
428
+ icons: {
429
+ icon: '/favicon.ico',
430
+ shortcut: '/favicon-16x16.png',
431
+ apple: '/apple-touch-icon.png',
432
+ },
433
+
434
+ // Manifest
435
+ manifest: '/site.webmanifest',
436
+
437
+ // Open Graph
438
+ openGraph: {
439
+ type: 'website',
440
+ locale: '${locale || 'en_US'}',
441
+ url: '${siteUrl}',
442
+ siteName: '${siteName}',
443
+ title: '${title || siteName}',
444
+ description: '${description || `${siteName} - A compelling description.`}',
445
+ images: [
446
+ {
447
+ url: '${image || '/og-image.png'}',
448
+ width: 1200,
449
+ height: 630,
450
+ alt: '${siteName}',
451
+ },
452
+ ],
453
+ },
454
+
455
+ // Twitter
456
+ twitter: {
457
+ card: 'summary_large_image',
458
+ title: '${title || siteName}',
459
+ description: '${description || `${siteName} - A compelling description.`}',
460
+ images: ['${image || '/og-image.png'}'],
461
+ ${twitterHandle ? `site: '${twitterHandle}',
462
+ creator: '${twitterHandle}',` : ''}
463
+ },
464
+
465
+ // Verification (add your IDs)
466
+ verification: {
467
+ // google: 'your-google-verification-code',
468
+ // yandex: 'your-yandex-verification-code',
469
+ // bing: 'your-bing-verification-code',
470
+ },
471
+
472
+ // Alternate languages (uncomment and customize)
473
+ // alternates: {
474
+ // canonical: '${siteUrl}',
475
+ // languages: {
476
+ // 'en-US': '${siteUrl}/en',
477
+ // 'es-ES': '${siteUrl}/es',
478
+ // },
479
+ // },
480
+
481
+ // Category
482
+ category: 'technology',
483
+ };
484
+
485
+ /**
486
+ * Viewport configuration
487
+ * Separated from metadata in Next.js 14+
488
+ */
489
+ export const viewport: Viewport = {
490
+ themeColor: [
491
+ { media: '(prefers-color-scheme: light)', color: '#ffffff' },
492
+ { media: '(prefers-color-scheme: dark)', color: '#000000' },
493
+ ],
494
+ width: 'device-width',
495
+ initialScale: 1,
496
+ maximumScale: 5,
497
+ };
498
+
499
+ export default function RootLayout({
500
+ children,
501
+ }: {
502
+ children: React.ReactNode;
503
+ }) {
504
+ return (
505
+ <html lang="${(locale || 'en_US').split('_')[0]}" className={inter.className}>
506
+ <body>
507
+ {children}
508
+
509
+ {/* JSON-LD Organization Schema */}
510
+ <script
511
+ type="application/ld+json"
512
+ dangerouslySetInnerHTML={{
513
+ __html: JSON.stringify({
514
+ '@context': 'https://schema.org',
515
+ '@type': 'Organization',
516
+ name: '${siteName}',
517
+ url: '${siteUrl}',
518
+ logo: '${siteUrl}/logo.png',
519
+ sameAs: [
520
+ // Add your social profiles
521
+ // 'https://twitter.com/yourhandle',
522
+ // 'https://linkedin.com/company/yourcompany',
523
+ ],
524
+ }),
525
+ }}
526
+ />
527
+ </body>
528
+ </html>
529
+ );
530
+ }`,
531
+ explanation: `Next.js App Router layout with comprehensive SEO:
532
+ • Metadata API with title templates
533
+ • Full Open Graph and Twitter Card support
534
+ • Viewport configuration (Next.js 14+)
535
+ • JSON-LD Organization schema
536
+ • Verification tags for search consoles
537
+ • Internationalization ready
538
+ • Web font optimization with next/font`,
539
+ additionalFiles: [
540
+ {
541
+ file: 'app/robots.ts',
542
+ code: `import type { MetadataRoute } from 'next';
543
+
544
+ export default function robots(): MetadataRoute.Robots {
545
+ const baseUrl = '${siteUrl}';
546
+
547
+ return {
548
+ rules: [
549
+ {
550
+ userAgent: '*',
551
+ allow: '/',
552
+ disallow: ['/api/', '/admin/', '/_next/', '/private/'],
553
+ },
554
+ {
555
+ userAgent: 'GPTBot',
556
+ allow: '/',
557
+ },
558
+ ],
559
+ sitemap: \`\${baseUrl}/sitemap.xml\`,
560
+ };
561
+ }`,
562
+ explanation: 'Robots.txt with AI crawler support.',
563
+ },
564
+ {
565
+ file: 'app/sitemap.ts',
566
+ code: `import type { MetadataRoute } from 'next';
567
+
568
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
569
+ const baseUrl = '${siteUrl}';
570
+
571
+ // Static pages
572
+ const staticPages: MetadataRoute.Sitemap = [
573
+ {
574
+ url: baseUrl,
575
+ lastModified: new Date(),
576
+ changeFrequency: 'daily',
577
+ priority: 1,
578
+ },
579
+ {
580
+ url: \`\${baseUrl}/about\`,
581
+ lastModified: new Date(),
582
+ changeFrequency: 'monthly',
583
+ priority: 0.8,
584
+ },
585
+ {
586
+ url: \`\${baseUrl}/pricing\`,
587
+ lastModified: new Date(),
588
+ changeFrequency: 'weekly',
589
+ priority: 0.9,
590
+ },
591
+ {
592
+ url: \`\${baseUrl}/blog\`,
593
+ lastModified: new Date(),
594
+ changeFrequency: 'daily',
595
+ priority: 0.8,
596
+ },
597
+ ];
598
+
599
+ // Dynamic pages - fetch from your database/CMS
600
+ // const posts = await db.post.findMany({ select: { slug: true, updatedAt: true } });
601
+ // const dynamicPages = posts.map((post) => ({
602
+ // url: \`\${baseUrl}/blog/\${post.slug}\`,
603
+ // lastModified: post.updatedAt,
604
+ // changeFrequency: 'weekly' as const,
605
+ // priority: 0.7,
606
+ // }));
607
+
608
+ return [...staticPages];
609
+ }`,
610
+ explanation: 'Dynamic sitemap generator.',
611
+ },
612
+ {
613
+ file: 'lib/seo.ts',
614
+ code: `import type { Metadata } from 'next';
615
+
616
+ const baseUrl = '${siteUrl}';
617
+ const siteName = '${siteName}';
618
+
619
+ interface PageSEOProps {
620
+ title: string;
621
+ description: string;
622
+ path?: string;
623
+ image?: string;
624
+ type?: 'website' | 'article';
625
+ publishedTime?: string;
626
+ modifiedTime?: string;
627
+ authors?: string[];
628
+ tags?: string[];
629
+ noIndex?: boolean;
630
+ }
631
+
632
+ /**
633
+ * Generate metadata for a page
634
+ * Use in page.tsx: export const metadata = generateMetadata({ ... })
635
+ */
636
+ export function generatePageMetadata({
637
+ title,
638
+ description,
639
+ path = '',
640
+ image,
641
+ type = 'website',
642
+ publishedTime,
643
+ modifiedTime,
644
+ authors,
645
+ tags,
646
+ noIndex = false,
647
+ }: PageSEOProps): Metadata {
648
+ const url = \`\${baseUrl}\${path}\`;
649
+ const ogImage = image || '/og-image.png';
650
+
651
+ return {
652
+ title,
653
+ description,
654
+
655
+ robots: noIndex ? { index: false, follow: false } : undefined,
656
+
657
+ alternates: {
658
+ canonical: url,
659
+ },
660
+
661
+ openGraph: {
662
+ title,
663
+ description,
664
+ url,
665
+ siteName,
666
+ type,
667
+ images: [{ url: ogImage, width: 1200, height: 630 }],
668
+ ...(type === 'article' && {
669
+ publishedTime,
670
+ modifiedTime,
671
+ authors,
672
+ tags,
673
+ }),
674
+ },
675
+
676
+ twitter: {
677
+ card: 'summary_large_image',
678
+ title,
679
+ description,
680
+ images: [ogImage],
681
+ },
682
+ };
683
+ }
684
+
685
+ /**
686
+ * Generate JSON-LD for articles
687
+ */
688
+ export function generateArticleJsonLd(article: {
689
+ title: string;
690
+ description: string;
691
+ url: string;
692
+ image: string;
693
+ datePublished: string;
694
+ dateModified?: string;
695
+ author: { name: string; url?: string };
696
+ }) {
697
+ return {
698
+ '@context': 'https://schema.org',
699
+ '@type': 'Article',
700
+ headline: article.title,
701
+ description: article.description,
702
+ url: article.url,
703
+ image: article.image,
704
+ datePublished: article.datePublished,
705
+ dateModified: article.dateModified || article.datePublished,
706
+ author: {
707
+ '@type': 'Person',
708
+ name: article.author.name,
709
+ url: article.author.url,
710
+ },
711
+ publisher: {
712
+ '@type': 'Organization',
713
+ name: siteName,
714
+ logo: {
715
+ '@type': 'ImageObject',
716
+ url: \`\${baseUrl}/logo.png\`,
717
+ },
718
+ },
719
+ };
720
+ }
721
+
722
+ /**
723
+ * Generate JSON-LD for products
724
+ */
725
+ export function generateProductJsonLd(product: {
726
+ name: string;
727
+ description: string;
728
+ image: string;
729
+ price: number;
730
+ currency?: string;
731
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
732
+ rating?: { value: number; count: number };
733
+ brand?: string;
734
+ sku?: string;
735
+ }) {
736
+ return {
737
+ '@context': 'https://schema.org',
738
+ '@type': 'Product',
739
+ name: product.name,
740
+ description: product.description,
741
+ image: product.image,
742
+ brand: product.brand ? { '@type': 'Brand', name: product.brand } : undefined,
743
+ sku: product.sku,
744
+ offers: {
745
+ '@type': 'Offer',
746
+ price: product.price,
747
+ priceCurrency: product.currency || 'USD',
748
+ availability: \`https://schema.org/\${product.availability || 'InStock'}\`,
749
+ },
750
+ ...(product.rating && {
751
+ aggregateRating: {
752
+ '@type': 'AggregateRating',
753
+ ratingValue: product.rating.value,
754
+ reviewCount: product.rating.count,
755
+ },
756
+ }),
757
+ };
758
+ }
759
+
760
+ /**
761
+ * Generate JSON-LD for FAQ pages
762
+ */
763
+ export function generateFAQJsonLd(items: { question: string; answer: string }[]) {
764
+ return {
765
+ '@context': 'https://schema.org',
766
+ '@type': 'FAQPage',
767
+ mainEntity: items.map(item => ({
768
+ '@type': 'Question',
769
+ name: item.question,
770
+ acceptedAnswer: {
771
+ '@type': 'Answer',
772
+ text: item.answer,
773
+ },
774
+ })),
775
+ };
776
+ }`,
777
+ explanation: 'SEO utility functions for generating metadata and JSON-LD.',
778
+ },
779
+ ],
780
+ };
781
+ }
782
+
783
+ // ============================================================================
784
+ // NUXT 3 - Comprehensive useHead composable
785
+ // ============================================================================
786
+
787
+ export function generateNuxtSEOHead(options: MetaFixOptions): GeneratedCode {
788
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
789
+
790
+ return {
791
+ file: 'composables/useSEO.ts',
792
+ code: `/**
793
+ * Comprehensive SEO composable for Nuxt 3
794
+ *
795
+ * Features:
796
+ * - Full Open Graph support
797
+ * - Twitter Cards
798
+ * - JSON-LD structured data
799
+ * - Canonical URLs
800
+ * - Robots directives
801
+ * - Internationalization
802
+ */
803
+
804
+ interface SEOOptions {
805
+ title?: string;
806
+ description?: string;
807
+ image?: string;
808
+ url?: string;
809
+ type?: 'website' | 'article' | 'product';
810
+
811
+ // Article-specific
812
+ publishedTime?: string;
813
+ modifiedTime?: string;
814
+ author?: string;
815
+ tags?: string[];
816
+
817
+ // Robots
818
+ noIndex?: boolean;
819
+ noFollow?: boolean;
820
+
821
+ // Structured data
822
+ schema?: Record<string, unknown> | Record<string, unknown>[];
823
+ }
824
+
825
+ const SITE_NAME = '${siteName}';
826
+ const SITE_URL = '${siteUrl}';
827
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
828
+ const TWITTER_HANDLE = '${twitterHandle || ''}';
829
+ const DEFAULT_LOCALE = '${locale || 'en_US'}';
830
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
831
+
832
+ export function useSEO(options: SEOOptions = {}) {
833
+ const route = useRoute();
834
+
835
+ const {
836
+ title,
837
+ description = DEFAULT_DESCRIPTION,
838
+ image = DEFAULT_IMAGE,
839
+ url,
840
+ type = 'website',
841
+ publishedTime,
842
+ modifiedTime,
843
+ author,
844
+ tags,
845
+ noIndex = false,
846
+ noFollow = false,
847
+ schema,
848
+ } = options;
849
+
850
+ const pageUrl = url || \`\${SITE_URL}\${route.path}\`;
851
+ const fullTitle = title
852
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
853
+ : SITE_NAME;
854
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
855
+
856
+ const robotsContent = [
857
+ noIndex ? 'noindex' : 'index',
858
+ noFollow ? 'nofollow' : 'follow',
859
+ ].join(', ');
860
+
861
+ // Build meta array
862
+ const meta = [
863
+ { name: 'description', content: description },
864
+ { name: 'robots', content: robotsContent },
865
+
866
+ // Open Graph
867
+ { property: 'og:type', content: type },
868
+ { property: 'og:url', content: pageUrl },
869
+ { property: 'og:title', content: fullTitle },
870
+ { property: 'og:description', content: description },
871
+ { property: 'og:image', content: imageUrl },
872
+ { property: 'og:image:width', content: '1200' },
873
+ { property: 'og:image:height', content: '630' },
874
+ { property: 'og:site_name', content: SITE_NAME },
875
+ { property: 'og:locale', content: DEFAULT_LOCALE },
876
+
877
+ // Twitter
878
+ { name: 'twitter:card', content: 'summary_large_image' },
879
+ { name: 'twitter:title', content: fullTitle },
880
+ { name: 'twitter:description', content: description },
881
+ { name: 'twitter:image', content: imageUrl },
882
+ ];
883
+
884
+ // Add Twitter handle if configured
885
+ if (TWITTER_HANDLE) {
886
+ meta.push(
887
+ { name: 'twitter:site', content: TWITTER_HANDLE },
888
+ { name: 'twitter:creator', content: TWITTER_HANDLE }
889
+ );
890
+ }
891
+
892
+ // Add article-specific meta
893
+ if (type === 'article') {
894
+ if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime });
895
+ if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime });
896
+ if (author) meta.push({ property: 'article:author', content: author });
897
+ tags?.forEach(tag => meta.push({ property: 'article:tag', content: tag }));
898
+ }
899
+
900
+ // Build JSON-LD
901
+ const defaultSchema = {
902
+ '@context': 'https://schema.org',
903
+ '@type': 'WebSite',
904
+ name: SITE_NAME,
905
+ url: SITE_URL,
906
+ };
907
+
908
+ const jsonLd = schema
909
+ ? Array.isArray(schema)
910
+ ? [defaultSchema, ...schema]
911
+ : [defaultSchema, schema]
912
+ : [defaultSchema];
913
+
914
+ useHead({
915
+ title: fullTitle,
916
+ meta,
917
+ link: [
918
+ { rel: 'canonical', href: pageUrl },
919
+ ],
920
+ script: [
921
+ {
922
+ type: 'application/ld+json',
923
+ innerHTML: JSON.stringify(jsonLd),
924
+ },
925
+ ],
926
+ });
927
+ }
928
+
929
+ /**
930
+ * Schema generators for common types
931
+ */
932
+ export const Schema = {
933
+ article: (data: {
934
+ headline: string;
935
+ description: string;
936
+ image: string;
937
+ datePublished: string;
938
+ dateModified?: string;
939
+ author: { name: string; url?: string };
940
+ }) => ({
941
+ '@context': 'https://schema.org',
942
+ '@type': 'Article',
943
+ headline: data.headline,
944
+ description: data.description,
945
+ image: data.image,
946
+ datePublished: data.datePublished,
947
+ dateModified: data.dateModified || data.datePublished,
948
+ author: { '@type': 'Person', ...data.author },
949
+ publisher: {
950
+ '@type': 'Organization',
951
+ name: SITE_NAME,
952
+ url: SITE_URL,
953
+ },
954
+ }),
955
+
956
+ product: (data: {
957
+ name: string;
958
+ description: string;
959
+ image: string;
960
+ price: number;
961
+ currency?: string;
962
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
963
+ }) => ({
964
+ '@context': 'https://schema.org',
965
+ '@type': 'Product',
966
+ name: data.name,
967
+ description: data.description,
968
+ image: data.image,
969
+ offers: {
970
+ '@type': 'Offer',
971
+ price: data.price,
972
+ priceCurrency: data.currency || 'USD',
973
+ availability: \`https://schema.org/\${data.availability || 'InStock'}\`,
974
+ },
975
+ }),
976
+
977
+ faq: (items: { question: string; answer: string }[]) => ({
978
+ '@context': 'https://schema.org',
979
+ '@type': 'FAQPage',
980
+ mainEntity: items.map(item => ({
981
+ '@type': 'Question',
982
+ name: item.question,
983
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
984
+ })),
985
+ }),
986
+
987
+ breadcrumb: (items: { name: string; url: string }[]) => ({
988
+ '@context': 'https://schema.org',
989
+ '@type': 'BreadcrumbList',
990
+ itemListElement: items.map((item, i) => ({
991
+ '@type': 'ListItem',
992
+ position: i + 1,
993
+ name: item.name,
994
+ item: item.url,
995
+ })),
996
+ }),
997
+ };`,
998
+ explanation: `Nuxt 3 comprehensive SEO composable with:
999
+ • Full useHead integration
1000
+ • Open Graph with article support
1001
+ • Twitter Cards
1002
+ • JSON-LD schema generators
1003
+ • Robots directives
1004
+ • Canonical URLs
1005
+
1006
+ Usage: useSEO({ title: 'Page', description: '...' })`,
1007
+ additionalFiles: [
1008
+ {
1009
+ file: 'server/routes/sitemap.xml.ts',
1010
+ code: `import { SitemapStream, streamToPromise } from 'sitemap';
1011
+ import { Readable } from 'stream';
1012
+
1013
+ export default defineEventHandler(async () => {
1014
+ const baseUrl = '${siteUrl}';
1015
+
1016
+ // Define your pages
1017
+ const pages = [
1018
+ { url: '/', changefreq: 'daily', priority: 1 },
1019
+ { url: '/about', changefreq: 'monthly', priority: 0.8 },
1020
+ { url: '/pricing', changefreq: 'weekly', priority: 0.9 },
1021
+ { url: '/blog', changefreq: 'daily', priority: 0.8 },
1022
+ ];
1023
+
1024
+ // Add dynamic pages from your database
1025
+ // const posts = await $fetch('/api/posts');
1026
+ // posts.forEach(post => pages.push({
1027
+ // url: \`/blog/\${post.slug}\`,
1028
+ // changefreq: 'weekly',
1029
+ // priority: 0.7,
1030
+ // lastmod: post.updatedAt,
1031
+ // }));
1032
+
1033
+ const stream = new SitemapStream({ hostname: baseUrl });
1034
+
1035
+ return streamToPromise(Readable.from(pages).pipe(stream)).then((data) =>
1036
+ data.toString()
1037
+ );
1038
+ });`,
1039
+ explanation: 'Dynamic sitemap generator for Nuxt.',
1040
+ },
1041
+ {
1042
+ file: 'public/robots.txt',
1043
+ code: `User-agent: *
1044
+ Allow: /
1045
+ Disallow: /api/
1046
+ Disallow: /admin/
1047
+
1048
+ User-agent: GPTBot
1049
+ Allow: /
1050
+
1051
+ Sitemap: ${siteUrl}/sitemap.xml`,
1052
+ explanation: 'Robots.txt with AI crawler support.',
1053
+ },
1054
+ ],
1055
+ };
1056
+ }
1057
+
1058
+ // ============================================================================
1059
+ // VUE.JS (without Nuxt) - @unhead/vue
1060
+ // ============================================================================
1061
+
1062
+ export function generateVueSEOHead(options: MetaFixOptions): GeneratedCode {
1063
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
1064
+
1065
+ return {
1066
+ file: 'src/composables/useSEO.ts',
1067
+ code: `import { useHead, useServerHead } from '@unhead/vue';
1068
+ import { computed, unref, MaybeRef } from 'vue';
1069
+ import { useRoute } from 'vue-router';
1070
+
1071
+ interface SEOOptions {
1072
+ title?: MaybeRef<string>;
1073
+ description?: MaybeRef<string>;
1074
+ image?: MaybeRef<string>;
1075
+ type?: 'website' | 'article';
1076
+ noIndex?: boolean;
1077
+ schema?: Record<string, unknown>;
1078
+ }
1079
+
1080
+ const SITE_NAME = '${siteName}';
1081
+ const SITE_URL = '${siteUrl}';
1082
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
1083
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
1084
+ const TWITTER_HANDLE = '${twitterHandle || ''}';
1085
+
1086
+ export function useSEO(options: SEOOptions = {}) {
1087
+ const route = useRoute();
1088
+
1089
+ const title = computed(() => {
1090
+ const t = unref(options.title);
1091
+ return t ? (t.includes(SITE_NAME) ? t : \`\${t} | \${SITE_NAME}\`) : SITE_NAME;
1092
+ });
1093
+
1094
+ const description = computed(() => unref(options.description) || DEFAULT_DESCRIPTION);
1095
+ const image = computed(() => {
1096
+ const img = unref(options.image) || DEFAULT_IMAGE;
1097
+ return img.startsWith('http') ? img : \`\${SITE_URL}\${img}\`;
1098
+ });
1099
+ const url = computed(() => \`\${SITE_URL}\${route.path}\`);
1100
+
1101
+ useHead({
1102
+ title,
1103
+ meta: [
1104
+ { name: 'description', content: description },
1105
+ { name: 'robots', content: options.noIndex ? 'noindex, nofollow' : 'index, follow' },
1106
+
1107
+ // Open Graph
1108
+ { property: 'og:type', content: options.type || 'website' },
1109
+ { property: 'og:url', content: url },
1110
+ { property: 'og:title', content: title },
1111
+ { property: 'og:description', content: description },
1112
+ { property: 'og:image', content: image },
1113
+ { property: 'og:site_name', content: SITE_NAME },
1114
+
1115
+ // Twitter
1116
+ { name: 'twitter:card', content: 'summary_large_image' },
1117
+ { name: 'twitter:title', content: title },
1118
+ { name: 'twitter:description', content: description },
1119
+ { name: 'twitter:image', content: image },
1120
+ ...(TWITTER_HANDLE ? [
1121
+ { name: 'twitter:site', content: TWITTER_HANDLE },
1122
+ { name: 'twitter:creator', content: TWITTER_HANDLE },
1123
+ ] : []),
1124
+ ],
1125
+ link: [
1126
+ { rel: 'canonical', href: url },
1127
+ ],
1128
+ script: options.schema ? [
1129
+ { type: 'application/ld+json', innerHTML: JSON.stringify(options.schema) },
1130
+ ] : [],
1131
+ });
1132
+ }`,
1133
+ explanation: `Vue 3 SEO composable using @unhead/vue with:
1134
+ • Reactive title/description
1135
+ • Open Graph and Twitter Cards
1136
+ • JSON-LD schema support
1137
+ • Canonical URLs
1138
+
1139
+ Install: npm install @unhead/vue`,
1140
+ installCommands: ['npm install @unhead/vue'],
1141
+ additionalFiles: [
1142
+ {
1143
+ file: 'src/main.ts',
1144
+ code: `import { createApp } from 'vue';
1145
+ import { createHead } from '@unhead/vue';
1146
+ import { createRouter, createWebHistory } from 'vue-router';
1147
+ import App from './App.vue';
1148
+
1149
+ const app = createApp(App);
1150
+ const head = createHead();
1151
+ const router = createRouter({
1152
+ history: createWebHistory(),
1153
+ routes: [/* your routes */],
1154
+ });
1155
+
1156
+ app.use(head);
1157
+ app.use(router);
1158
+ app.mount('#app');`,
1159
+ explanation: 'Vue app setup with @unhead/vue.',
1160
+ },
1161
+ ],
1162
+ };
1163
+ }
1164
+
1165
+ // ============================================================================
1166
+ // ASTRO - Comprehensive BaseHead component
1167
+ // ============================================================================
1168
+
1169
+ export function generateAstroBaseHead(options: MetaFixOptions): GeneratedCode {
1170
+ const { siteName, siteUrl, description, image, twitterHandle, locale } = options;
1171
+
1172
+ return {
1173
+ file: 'src/components/BaseHead.astro',
1174
+ code: `---
1175
+ /**
1176
+ * Comprehensive SEO Head Component for Astro
1177
+ *
1178
+ * Features:
1179
+ * - Full Open Graph support
1180
+ * - Twitter Cards
1181
+ * - JSON-LD structured data
1182
+ * - Canonical URLs
1183
+ * - Robots directives
1184
+ * - Performance optimizations
1185
+ */
1186
+
1187
+ interface Props {
1188
+ title?: string;
1189
+ description?: string;
1190
+ image?: string;
1191
+ type?: 'website' | 'article';
1192
+ publishedTime?: string;
1193
+ modifiedTime?: string;
1194
+ author?: string;
1195
+ tags?: string[];
1196
+ noIndex?: boolean;
1197
+ schema?: Record<string, unknown>;
1198
+ }
1199
+
1200
+ const SITE_NAME = '${siteName}';
1201
+ const SITE_URL = '${siteUrl}';
1202
+ const DEFAULT_IMAGE = '${image || '/og-image.png'}';
1203
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
1204
+ const TWITTER_HANDLE = '${twitterHandle || ''}';
1205
+ const DEFAULT_LOCALE = '${locale || 'en_US'}';
1206
+
1207
+ const {
1208
+ title,
1209
+ description = DEFAULT_DESCRIPTION,
1210
+ image = DEFAULT_IMAGE,
1211
+ type = 'website',
1212
+ publishedTime,
1213
+ modifiedTime,
1214
+ author,
1215
+ tags,
1216
+ noIndex = false,
1217
+ schema,
1218
+ } = Astro.props;
1219
+
1220
+ const canonicalURL = new URL(Astro.url.pathname, Astro.site || SITE_URL);
1221
+ const fullTitle = title
1222
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
1223
+ : SITE_NAME;
1224
+ const imageURL = new URL(image, Astro.site || SITE_URL);
1225
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
1226
+
1227
+ // Default website schema
1228
+ const defaultSchema = {
1229
+ '@context': 'https://schema.org',
1230
+ '@type': 'WebSite',
1231
+ name: SITE_NAME,
1232
+ url: SITE_URL,
1233
+ };
1234
+
1235
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
1236
+ ---
1237
+
1238
+ <!-- Global Metadata -->
1239
+ <meta charset="utf-8" />
1240
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
1241
+ <meta name="generator" content={Astro.generator} />
1242
+
1243
+ <!-- Favicon -->
1244
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
1245
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
1246
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
1247
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
1248
+ <link rel="manifest" href="/site.webmanifest" />
1249
+
1250
+ <!-- Canonical URL -->
1251
+ <link rel="canonical" href={canonicalURL} />
1252
+
1253
+ <!-- Primary Meta Tags -->
1254
+ <title>{fullTitle}</title>
1255
+ <meta name="title" content={fullTitle} />
1256
+ <meta name="description" content={description} />
1257
+ <meta name="robots" content={robotsContent} />
1258
+
1259
+ <!-- Theme -->
1260
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
1261
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)" />
1262
+
1263
+ <!-- Open Graph / Facebook -->
1264
+ <meta property="og:type" content={type} />
1265
+ <meta property="og:url" content={canonicalURL} />
1266
+ <meta property="og:title" content={fullTitle} />
1267
+ <meta property="og:description" content={description} />
1268
+ <meta property="og:image" content={imageURL} />
1269
+ <meta property="og:image:width" content="1200" />
1270
+ <meta property="og:image:height" content="630" />
1271
+ <meta property="og:image:alt" content={fullTitle} />
1272
+ <meta property="og:site_name" content={SITE_NAME} />
1273
+ <meta property="og:locale" content={DEFAULT_LOCALE} />
1274
+
1275
+ {type === 'article' && publishedTime && (
1276
+ <meta property="article:published_time" content={publishedTime} />
1277
+ )}
1278
+ {type === 'article' && modifiedTime && (
1279
+ <meta property="article:modified_time" content={modifiedTime} />
1280
+ )}
1281
+ {type === 'article' && author && (
1282
+ <meta property="article:author" content={author} />
1283
+ )}
1284
+ {type === 'article' && tags?.map((tag) => (
1285
+ <meta property="article:tag" content={tag} />
1286
+ ))}
1287
+
1288
+ <!-- Twitter -->
1289
+ <meta name="twitter:card" content="summary_large_image" />
1290
+ <meta name="twitter:url" content={canonicalURL} />
1291
+ <meta name="twitter:title" content={fullTitle} />
1292
+ <meta name="twitter:description" content={description} />
1293
+ <meta name="twitter:image" content={imageURL} />
1294
+ <meta name="twitter:image:alt" content={fullTitle} />
1295
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
1296
+ {TWITTER_HANDLE && <meta name="twitter:creator" content={TWITTER_HANDLE} />}
1297
+
1298
+ <!-- Performance: Preconnect to external origins -->
1299
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
1300
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
1301
+
1302
+ <!-- JSON-LD Structured Data -->
1303
+ <script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />`,
1304
+ explanation: `Astro comprehensive SEO component with:
1305
+ • Full Open Graph with article support
1306
+ • Twitter Cards
1307
+ • JSON-LD structured data
1308
+ • Performance optimizations (preconnect)
1309
+ • Theme color for PWA
1310
+ • Favicon configuration
1311
+
1312
+ Usage: <BaseHead title="Page" description="..." />`,
1313
+ additionalFiles: [
1314
+ {
1315
+ file: 'src/layouts/BaseLayout.astro',
1316
+ code: `---
1317
+ import BaseHead from '../components/BaseHead.astro';
1318
+
1319
+ interface Props {
1320
+ title?: string;
1321
+ description?: string;
1322
+ image?: string;
1323
+ type?: 'website' | 'article';
1324
+ schema?: Record<string, unknown>;
1325
+ }
1326
+
1327
+ const { title, description, image, type, schema } = Astro.props;
1328
+ ---
1329
+
1330
+ <!DOCTYPE html>
1331
+ <html lang="${(locale || 'en_US').split('_')[0]}">
1332
+ <head>
1333
+ <BaseHead
1334
+ title={title}
1335
+ description={description}
1336
+ image={image}
1337
+ type={type}
1338
+ schema={schema}
1339
+ />
1340
+ </head>
1341
+ <body>
1342
+ <slot />
1343
+ </body>
1344
+ </html>`,
1345
+ explanation: 'Base layout using the SEO head component.',
1346
+ },
1347
+ {
1348
+ file: 'public/robots.txt',
1349
+ code: `User-agent: *
1350
+ Allow: /
1351
+ Disallow: /api/
1352
+
1353
+ User-agent: GPTBot
1354
+ Allow: /
1355
+
1356
+ Sitemap: ${siteUrl}/sitemap-index.xml`,
1357
+ explanation: 'Robots.txt with AI crawler support.',
1358
+ },
1359
+ ],
1360
+ };
1361
+ }
1362
+
1363
+ // ============================================================================
1364
+ // SVELTEKIT - Comprehensive svelte:head
1365
+ // ============================================================================
1366
+
1367
+ export function generateSvelteKitSEOHead(options: MetaFixOptions): GeneratedCode {
1368
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
1369
+
1370
+ return {
1371
+ file: 'src/lib/components/SEOHead.svelte',
1372
+ code: `<script lang="ts">
1373
+ import { page } from '$app/stores';
1374
+
1375
+ export let title: string | undefined = undefined;
1376
+ export let description: string = '${description || `${siteName} - A compelling description.`}';
1377
+ export let image: string = '${image || `${siteUrl}/og-image.png`}';
1378
+ export let type: 'website' | 'article' = 'website';
1379
+ export let publishedTime: string | undefined = undefined;
1380
+ export let modifiedTime: string | undefined = undefined;
1381
+ export let author: string | undefined = undefined;
1382
+ export let tags: string[] = [];
1383
+ export let noIndex: boolean = false;
1384
+ export let schema: Record<string, unknown> | undefined = undefined;
1385
+
1386
+ const SITE_NAME = '${siteName}';
1387
+ const SITE_URL = '${siteUrl}';
1388
+ const TWITTER_HANDLE = '${twitterHandle || ''}';
1389
+
1390
+ $: fullTitle = title
1391
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
1392
+ : SITE_NAME;
1393
+ $: canonicalUrl = $page.url.href;
1394
+ $: imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
1395
+ $: robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
1396
+
1397
+ $: defaultSchema = {
1398
+ '@context': 'https://schema.org',
1399
+ '@type': 'WebSite',
1400
+ name: SITE_NAME,
1401
+ url: SITE_URL,
1402
+ };
1403
+
1404
+ $: jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
1405
+ </script>
1406
+
1407
+ <svelte:head>
1408
+ <!-- Primary Meta Tags -->
1409
+ <title>{fullTitle}</title>
1410
+ <meta name="title" content={fullTitle} />
1411
+ <meta name="description" content={description} />
1412
+ <meta name="robots" content={robotsContent} />
1413
+ <link rel="canonical" href={canonicalUrl} />
1414
+
1415
+ <!-- Open Graph / Facebook -->
1416
+ <meta property="og:type" content={type} />
1417
+ <meta property="og:url" content={canonicalUrl} />
1418
+ <meta property="og:title" content={fullTitle} />
1419
+ <meta property="og:description" content={description} />
1420
+ <meta property="og:image" content={imageUrl} />
1421
+ <meta property="og:image:width" content="1200" />
1422
+ <meta property="og:image:height" content="630" />
1423
+ <meta property="og:site_name" content={SITE_NAME} />
1424
+
1425
+ {#if type === 'article'}
1426
+ {#if publishedTime}
1427
+ <meta property="article:published_time" content={publishedTime} />
1428
+ {/if}
1429
+ {#if modifiedTime}
1430
+ <meta property="article:modified_time" content={modifiedTime} />
1431
+ {/if}
1432
+ {#if author}
1433
+ <meta property="article:author" content={author} />
1434
+ {/if}
1435
+ {#each tags as tag}
1436
+ <meta property="article:tag" content={tag} />
1437
+ {/each}
1438
+ {/if}
1439
+
1440
+ <!-- Twitter -->
1441
+ <meta name="twitter:card" content="summary_large_image" />
1442
+ <meta name="twitter:title" content={fullTitle} />
1443
+ <meta name="twitter:description" content={description} />
1444
+ <meta name="twitter:image" content={imageUrl} />
1445
+ {#if TWITTER_HANDLE}
1446
+ <meta name="twitter:site" content={TWITTER_HANDLE} />
1447
+ <meta name="twitter:creator" content={TWITTER_HANDLE} />
1448
+ {/if}
1449
+
1450
+ <!-- JSON-LD Structured Data -->
1451
+ {@html \`<script type="application/ld+json">\${JSON.stringify(jsonLd)}</script>\`}
1452
+ </svelte:head>`,
1453
+ explanation: `SvelteKit comprehensive SEO component with:
1454
+ • Reactive props
1455
+ • Full Open Graph with article support
1456
+ • Twitter Cards
1457
+ • JSON-LD structured data
1458
+ • Robots directives
1459
+
1460
+ Usage: <SEOHead title="Page" description="..." />`,
1461
+ additionalFiles: [
1462
+ {
1463
+ file: 'src/routes/+layout.svelte',
1464
+ code: `<script lang="ts">
1465
+ import '../app.css';
1466
+ </script>
1467
+
1468
+ <slot />`,
1469
+ explanation: 'Root layout.',
1470
+ },
1471
+ {
1472
+ file: 'static/robots.txt',
1473
+ code: `User-agent: *
1474
+ Allow: /
1475
+ Disallow: /api/
1476
+
1477
+ User-agent: GPTBot
1478
+ Allow: /
1479
+
1480
+ Sitemap: ${siteUrl}/sitemap.xml`,
1481
+ explanation: 'Robots.txt with AI crawler support.',
1482
+ },
1483
+ ],
1484
+ };
1485
+ }
1486
+
1487
+ // ============================================================================
1488
+ // ANGULAR - Comprehensive SEO Service
1489
+ // ============================================================================
1490
+
1491
+ export function generateAngularSEOService(options: MetaFixOptions): GeneratedCode {
1492
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
1493
+
1494
+ return {
1495
+ file: 'src/app/core/services/seo.service.ts',
1496
+ code: `import { Injectable, Inject } from '@angular/core';
1497
+ import { Meta, Title } from '@angular/platform-browser';
1498
+ import { Router, NavigationEnd } from '@angular/router';
1499
+ import { DOCUMENT } from '@angular/common';
1500
+ import { filter } from 'rxjs/operators';
1501
+
1502
+ interface SEOConfig {
1503
+ title?: string;
1504
+ description?: string;
1505
+ image?: string;
1506
+ type?: 'website' | 'article';
1507
+ publishedTime?: string;
1508
+ modifiedTime?: string;
1509
+ author?: string;
1510
+ tags?: string[];
1511
+ noIndex?: boolean;
1512
+ schema?: Record<string, unknown>;
1513
+ }
1514
+
1515
+ @Injectable({
1516
+ providedIn: 'root'
1517
+ })
1518
+ export class SEOService {
1519
+ private readonly siteName = '${siteName}';
1520
+ private readonly siteUrl = '${siteUrl}';
1521
+ private readonly defaultDescription = '${description || `${siteName} - A compelling description.`}';
1522
+ private readonly defaultImage = '${image || `${siteUrl}/og-image.png`}';
1523
+ private readonly twitterHandle = '${twitterHandle || ''}';
1524
+
1525
+ constructor(
1526
+ private meta: Meta,
1527
+ private titleService: Title,
1528
+ private router: Router,
1529
+ @Inject(DOCUMENT) private document: Document
1530
+ ) {
1531
+ // Update canonical URL on route change
1532
+ this.router.events.pipe(
1533
+ filter(event => event instanceof NavigationEnd)
1534
+ ).subscribe(() => {
1535
+ this.updateCanonical();
1536
+ });
1537
+ }
1538
+
1539
+ /**
1540
+ * Update all SEO meta tags
1541
+ */
1542
+ updateMeta(config: SEOConfig = {}): void {
1543
+ const {
1544
+ title,
1545
+ description = this.defaultDescription,
1546
+ image = this.defaultImage,
1547
+ type = 'website',
1548
+ publishedTime,
1549
+ modifiedTime,
1550
+ author,
1551
+ tags,
1552
+ noIndex = false,
1553
+ schema,
1554
+ } = config;
1555
+
1556
+ const fullTitle = title
1557
+ ? (title.includes(this.siteName) ? title : \`\${title} | \${this.siteName}\`)
1558
+ : this.siteName;
1559
+ const pageUrl = this.siteUrl + this.router.url;
1560
+ const imageUrl = image.startsWith('http') ? image : \`\${this.siteUrl}\${image}\`;
1561
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
1562
+
1563
+ // Title
1564
+ this.titleService.setTitle(fullTitle);
1565
+
1566
+ // Primary Meta Tags
1567
+ this.setMetaTag('description', description);
1568
+ this.setMetaTag('robots', robotsContent);
1569
+
1570
+ // Open Graph
1571
+ this.setMetaProperty('og:type', type);
1572
+ this.setMetaProperty('og:url', pageUrl);
1573
+ this.setMetaProperty('og:title', fullTitle);
1574
+ this.setMetaProperty('og:description', description);
1575
+ this.setMetaProperty('og:image', imageUrl);
1576
+ this.setMetaProperty('og:image:width', '1200');
1577
+ this.setMetaProperty('og:image:height', '630');
1578
+ this.setMetaProperty('og:site_name', this.siteName);
1579
+
1580
+ // Article-specific
1581
+ if (type === 'article') {
1582
+ if (publishedTime) this.setMetaProperty('article:published_time', publishedTime);
1583
+ if (modifiedTime) this.setMetaProperty('article:modified_time', modifiedTime);
1584
+ if (author) this.setMetaProperty('article:author', author);
1585
+ tags?.forEach(tag => this.setMetaProperty('article:tag', tag));
1586
+ }
1587
+
1588
+ // Twitter
1589
+ this.setMetaTag('twitter:card', 'summary_large_image');
1590
+ this.setMetaTag('twitter:title', fullTitle);
1591
+ this.setMetaTag('twitter:description', description);
1592
+ this.setMetaTag('twitter:image', imageUrl);
1593
+ if (this.twitterHandle) {
1594
+ this.setMetaTag('twitter:site', this.twitterHandle);
1595
+ this.setMetaTag('twitter:creator', this.twitterHandle);
1596
+ }
1597
+
1598
+ // Update canonical
1599
+ this.updateCanonical(pageUrl);
1600
+
1601
+ // Update JSON-LD
1602
+ this.updateJsonLd(schema);
1603
+ }
1604
+
1605
+ private setMetaTag(name: string, content: string): void {
1606
+ this.meta.updateTag({ name, content });
1607
+ }
1608
+
1609
+ private setMetaProperty(property: string, content: string): void {
1610
+ this.meta.updateTag({ property, content });
1611
+ }
1612
+
1613
+ private updateCanonical(url?: string): void {
1614
+ const canonicalUrl = url || this.siteUrl + this.router.url;
1615
+ let link = this.document.querySelector('link[rel="canonical"]') as HTMLLinkElement;
1616
+
1617
+ if (!link) {
1618
+ link = this.document.createElement('link');
1619
+ link.setAttribute('rel', 'canonical');
1620
+ this.document.head.appendChild(link);
1621
+ }
1622
+
1623
+ link.setAttribute('href', canonicalUrl);
1624
+ }
1625
+
1626
+ private updateJsonLd(schema?: Record<string, unknown>): void {
1627
+ // Remove existing JSON-LD
1628
+ const existing = this.document.querySelector('script[type="application/ld+json"]');
1629
+ if (existing) existing.remove();
1630
+
1631
+ // Add new JSON-LD
1632
+ const defaultSchema = {
1633
+ '@context': 'https://schema.org',
1634
+ '@type': 'WebSite',
1635
+ name: this.siteName,
1636
+ url: this.siteUrl,
1637
+ };
1638
+
1639
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
1640
+
1641
+ const script = this.document.createElement('script');
1642
+ script.type = 'application/ld+json';
1643
+ script.text = JSON.stringify(jsonLd);
1644
+ this.document.head.appendChild(script);
1645
+ }
1646
+
1647
+ /**
1648
+ * Generate Article schema
1649
+ */
1650
+ articleSchema(data: {
1651
+ headline: string;
1652
+ description: string;
1653
+ image: string;
1654
+ datePublished: string;
1655
+ dateModified?: string;
1656
+ author: { name: string; url?: string };
1657
+ }): Record<string, unknown> {
1658
+ return {
1659
+ '@context': 'https://schema.org',
1660
+ '@type': 'Article',
1661
+ headline: data.headline,
1662
+ description: data.description,
1663
+ image: data.image,
1664
+ datePublished: data.datePublished,
1665
+ dateModified: data.dateModified || data.datePublished,
1666
+ author: { '@type': 'Person', ...data.author },
1667
+ publisher: {
1668
+ '@type': 'Organization',
1669
+ name: this.siteName,
1670
+ url: this.siteUrl,
1671
+ },
1672
+ };
1673
+ }
1674
+
1675
+ /**
1676
+ * Generate Product schema
1677
+ */
1678
+ productSchema(data: {
1679
+ name: string;
1680
+ description: string;
1681
+ image: string;
1682
+ price: number;
1683
+ currency?: string;
1684
+ }): Record<string, unknown> {
1685
+ return {
1686
+ '@context': 'https://schema.org',
1687
+ '@type': 'Product',
1688
+ name: data.name,
1689
+ description: data.description,
1690
+ image: data.image,
1691
+ offers: {
1692
+ '@type': 'Offer',
1693
+ price: data.price,
1694
+ priceCurrency: data.currency || 'USD',
1695
+ availability: 'https://schema.org/InStock',
1696
+ },
1697
+ };
1698
+ }
1699
+
1700
+ /**
1701
+ * Generate FAQ schema
1702
+ */
1703
+ faqSchema(items: { question: string; answer: string }[]): Record<string, unknown> {
1704
+ return {
1705
+ '@context': 'https://schema.org',
1706
+ '@type': 'FAQPage',
1707
+ mainEntity: items.map(item => ({
1708
+ '@type': 'Question',
1709
+ name: item.question,
1710
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
1711
+ })),
1712
+ };
1713
+ }
1714
+ }`,
1715
+ explanation: `Angular comprehensive SEO service with:
1716
+ • Meta and Title service integration
1717
+ • Dynamic canonical URL updates
1718
+ • Full Open Graph with article support
1719
+ • Twitter Cards
1720
+ • JSON-LD schema generators
1721
+ • Automatic route change handling
1722
+
1723
+ Usage: Inject SEOService and call updateMeta()`,
1724
+ };
1725
+ }
1726
+
1727
+ // ============================================================================
1728
+ // Helper: Get framework-specific fix based on detected framework
1729
+ // ============================================================================
1730
+
1731
+ export function getFrameworkSpecificFix(
1732
+ framework: FrameworkInfo,
1733
+ options: MetaFixOptions
1734
+ ): GeneratedCode {
1735
+ const name = framework.name.toLowerCase();
1736
+
1737
+ if (name.includes('next')) {
1738
+ if (framework.router === 'app') {
1739
+ return generateNextJsAppRouterMetadata(options);
1740
+ } else {
1741
+ return generateNextJsPagesRouterHead(options);
1742
+ }
1743
+ }
1744
+
1745
+ if (name.includes('nuxt')) {
1746
+ return generateNuxtSEOHead(options);
1747
+ }
1748
+
1749
+ if (name.includes('vue')) {
1750
+ return generateVueSEOHead(options);
1751
+ }
1752
+
1753
+ if (name.includes('astro')) {
1754
+ return generateAstroBaseHead(options);
1755
+ }
1756
+
1757
+ if (name.includes('svelte')) {
1758
+ return generateSvelteKitSEOHead(options);
1759
+ }
1760
+
1761
+ if (name.includes('angular')) {
1762
+ return generateAngularSEOService(options);
1763
+ }
1764
+
1765
+ // Default: React with react-helmet-async
1766
+ return generateReactSEOHead(options);
1767
+ }
1768
+
1769
+ // ============================================================================
1770
+ // Next.js Pages Router (for completeness)
1771
+ // ============================================================================
1772
+
1773
+ export function generateNextJsPagesRouterHead(options: MetaFixOptions): GeneratedCode {
1774
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
1775
+
1776
+ return {
1777
+ file: 'components/SEOHead.tsx',
1778
+ code: `import Head from 'next/head';
1779
+ import { useRouter } from 'next/router';
1780
+
1781
+ interface SEOHeadProps {
1782
+ title?: string;
1783
+ description?: string;
1784
+ image?: string;
1785
+ type?: 'website' | 'article';
1786
+ publishedTime?: string;
1787
+ modifiedTime?: string;
1788
+ noIndex?: boolean;
1789
+ schema?: Record<string, unknown>;
1790
+ }
1791
+
1792
+ const SITE_NAME = '${siteName}';
1793
+ const SITE_URL = '${siteUrl}';
1794
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
1795
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
1796
+ const TWITTER_HANDLE = '${twitterHandle || ''}';
1797
+
1798
+ export function SEOHead({
1799
+ title,
1800
+ description = DEFAULT_DESCRIPTION,
1801
+ image = DEFAULT_IMAGE,
1802
+ type = 'website',
1803
+ publishedTime,
1804
+ modifiedTime,
1805
+ noIndex = false,
1806
+ schema,
1807
+ }: SEOHeadProps) {
1808
+ const router = useRouter();
1809
+
1810
+ const fullTitle = title
1811
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
1812
+ : SITE_NAME;
1813
+ const pageUrl = \`\${SITE_URL}\${router.asPath}\`;
1814
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
1815
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
1816
+
1817
+ const defaultSchema = {
1818
+ '@context': 'https://schema.org',
1819
+ '@type': 'WebSite',
1820
+ name: SITE_NAME,
1821
+ url: SITE_URL,
1822
+ };
1823
+
1824
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
1825
+
1826
+ return (
1827
+ <Head>
1828
+ <title>{fullTitle}</title>
1829
+ <meta name="description" content={description} />
1830
+ <meta name="robots" content={robotsContent} />
1831
+ <link rel="canonical" href={pageUrl} />
1832
+
1833
+ <meta property="og:type" content={type} />
1834
+ <meta property="og:url" content={pageUrl} />
1835
+ <meta property="og:title" content={fullTitle} />
1836
+ <meta property="og:description" content={description} />
1837
+ <meta property="og:image" content={imageUrl} />
1838
+ <meta property="og:site_name" content={SITE_NAME} />
1839
+
1840
+ {type === 'article' && publishedTime && (
1841
+ <meta property="article:published_time" content={publishedTime} />
1842
+ )}
1843
+ {type === 'article' && modifiedTime && (
1844
+ <meta property="article:modified_time" content={modifiedTime} />
1845
+ )}
1846
+
1847
+ <meta name="twitter:card" content="summary_large_image" />
1848
+ <meta name="twitter:title" content={fullTitle} />
1849
+ <meta name="twitter:description" content={description} />
1850
+ <meta name="twitter:image" content={imageUrl} />
1851
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
1852
+
1853
+ <script
1854
+ type="application/ld+json"
1855
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
1856
+ />
1857
+ </Head>
1858
+ );
1859
+ }`,
1860
+ explanation: `Next.js Pages Router SEO component with:
1861
+ • Full Open Graph support
1862
+ • Twitter Cards
1863
+ • JSON-LD structured data
1864
+ • Article metadata
1865
+ • Canonical URLs
1866
+
1867
+ Usage: <SEOHead title="Page" description="..." />`,
1868
+ };
1869
+ }