@rankcli/agent-runtime 0.0.7 → 0.0.9

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.
@@ -1,7 +1,13 @@
1
1
  /**
2
2
  * Framework-Specific SEO Fix Generators
3
3
  *
4
- * Generates proper code snippets for each framework's meta tag patterns.
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
5
11
  */
6
12
 
7
13
  import type { FrameworkInfo } from '../types.js';
@@ -12,6 +18,8 @@ export interface MetaFixOptions {
12
18
  title?: string;
13
19
  description?: string;
14
20
  image?: string;
21
+ twitterHandle?: string;
22
+ locale?: string;
15
23
  }
16
24
 
17
25
  export interface GeneratedCode {
@@ -20,70 +28,341 @@ export interface GeneratedCode {
20
28
  explanation: string;
21
29
  installCommands?: string[];
22
30
  imports?: string[];
31
+ additionalFiles?: { file: string; code: string; explanation: string }[];
23
32
  }
24
33
 
25
34
  // ============================================================================
26
- // REACT (Vite/CRA) - Client-side with react-helmet-async
35
+ // REACT (Vite/CRA) - Comprehensive SEO with react-helmet-async
27
36
  // ============================================================================
28
37
 
29
38
  export function generateReactSEOHead(options: MetaFixOptions): GeneratedCode {
30
- const { siteName, siteUrl, title, description, image } = options;
39
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
31
40
 
32
41
  return {
33
42
  file: 'src/components/SEOHead.tsx',
34
43
  code: `import { Helmet } from 'react-helmet-async';
35
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
+
36
68
  interface SEOHeadProps {
69
+ // Required
37
70
  title?: string;
38
71
  description?: string;
39
- image?: string;
72
+
73
+ // URLs
40
74
  url?: string;
41
- type?: 'website' | 'article';
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 }[];
42
100
  }
43
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
+
44
108
  export function SEOHead({
45
- title = '${title || `${siteName} - Your tagline here`}',
109
+ title,
46
110
  description = '${description || `${siteName} - A compelling description of your product or service.`}',
47
- image = '${image || `${siteUrl}/og-image.png`}',
48
- url = typeof window !== 'undefined' ? window.location.href : '${siteUrl}',
111
+ url,
112
+ canonical,
113
+ image = DEFAULT_IMAGE,
49
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,
50
125
  }: SEOHeadProps) {
51
- const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
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];
52
155
 
53
156
  return (
54
157
  <Helmet>
55
158
  {/* Primary Meta Tags */}
56
159
  <title>{fullTitle}</title>
160
+ <meta name="title" content={fullTitle} />
57
161
  <meta name="description" content={description} />
58
- <link rel="canonical" href={url} />
162
+ <meta name="robots" content={robotsContent} />
163
+ <link rel="canonical" href={canonicalUrl} />
59
164
 
60
165
  {/* Open Graph / Facebook */}
61
166
  <meta property="og:type" content={type} />
62
- <meta property="og:url" content={url} />
167
+ <meta property="og:url" content={pageUrl} />
63
168
  <meta property="og:title" content={fullTitle} />
64
169
  <meta property="og:description" content={description} />
65
- <meta property="og:image" content={image} />
66
- <meta property="og:site_name" content="${siteName}" />
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
+ ))}
67
193
 
68
194
  {/* Twitter */}
69
- <meta name="twitter:card" content="summary_large_image" />
70
- <meta name="twitter:url" content={url} />
195
+ <meta name="twitter:card" content={twitterCard} />
196
+ <meta name="twitter:url" content={pageUrl} />
71
197
  <meta name="twitter:title" content={fullTitle} />
72
198
  <meta name="twitter:description" content={description} />
73
- <meta name="twitter:image" content={image} />
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>
74
213
  </Helmet>
75
214
  );
76
- }`,
77
- explanation: 'React SEO component using react-helmet-async. Wrap your app in <HelmetProvider> and use <SEOHead /> on each page.',
78
- installCommands: ['npm install react-helmet-async'],
79
- imports: ["import { HelmetProvider } from 'react-helmet-async';"],
80
- };
81
215
  }
82
216
 
83
- export function generateReactAppWrapper(): GeneratedCode {
84
- return {
85
- file: 'src/main.tsx',
86
- code: `import React from 'react';
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';
87
366
  import ReactDOM from 'react-dom/client';
88
367
  import { HelmetProvider } from 'react-helmet-async';
89
368
  import App from './App';
@@ -96,35 +375,69 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
96
375
  </HelmetProvider>
97
376
  </React.StrictMode>,
98
377
  );`,
99
- explanation: 'Updated main.tsx with HelmetProvider wrapper for react-helmet-async.',
378
+ explanation: 'Updated main.tsx with HelmetProvider wrapper.',
379
+ },
380
+ ],
100
381
  };
101
382
  }
102
383
 
103
384
  // ============================================================================
104
- // NEXT.JS (App Router) - Server-side metadata export
385
+ // NEXT.JS (App Router) - Server-side metadata with full features
105
386
  // ============================================================================
106
387
 
107
388
  export function generateNextJsAppRouterMetadata(options: MetaFixOptions): GeneratedCode {
108
- const { siteName, siteUrl, title, description, image } = options;
389
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
109
390
 
110
391
  return {
111
392
  file: 'app/layout.tsx',
112
- code: `import type { Metadata } from 'next';
393
+ code: `import type { Metadata, Viewport } from 'next';
113
394
  import { Inter } from 'next/font/google';
114
395
  import './globals.css';
115
396
 
116
- const inter = Inter({ subsets: ['latin'] });
397
+ const inter = Inter({ subsets: ['latin'], display: 'swap' });
117
398
 
399
+ /**
400
+ * Default metadata for all pages
401
+ * Individual pages can override with their own metadata export
402
+ */
118
403
  export const metadata: Metadata = {
119
404
  metadataBase: new URL('${siteUrl}'),
405
+
406
+ // Default title with template
120
407
  title: {
121
408
  default: '${title || siteName}',
122
409
  template: \`%s | ${siteName}\`,
123
410
  },
411
+
124
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
125
438
  openGraph: {
126
439
  type: 'website',
127
- locale: 'en_US',
440
+ locale: '${locale || 'en_US'}',
128
441
  url: '${siteUrl}',
129
442
  siteName: '${siteName}',
130
443
  title: '${title || siteName}',
@@ -138,16 +451,49 @@ export const metadata: Metadata = {
138
451
  },
139
452
  ],
140
453
  },
454
+
455
+ // Twitter
141
456
  twitter: {
142
457
  card: 'summary_large_image',
143
458
  title: '${title || siteName}',
144
459
  description: '${description || `${siteName} - A compelling description.`}',
145
460
  images: ['${image || '/og-image.png'}'],
461
+ ${twitterHandle ? `site: '${twitterHandle}',
462
+ creator: '${twitterHandle}',` : ''}
146
463
  },
147
- robots: {
148
- index: true,
149
- follow: true,
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',
150
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,
151
497
  };
152
498
 
153
499
  export default function RootLayout({
@@ -156,148 +502,104 @@ export default function RootLayout({
156
502
  children: React.ReactNode;
157
503
  }) {
158
504
  return (
159
- <html lang="en">
160
- <body className={inter.className}>{children}</body>
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>
161
528
  </html>
162
529
  );
163
530
  }`,
164
- explanation: 'Next.js App Router layout with built-in Metadata API. Each page can override with its own metadata export.',
165
- };
166
- }
167
-
168
- export function generateNextJsPageMetadata(options: MetaFixOptions & { pageName: string }): GeneratedCode {
169
- const { siteName, description, pageName } = options;
170
-
171
- return {
172
- file: `app/${pageName}/page.tsx`,
173
- code: `import type { Metadata } from 'next';
174
-
175
- export const metadata: Metadata = {
176
- title: '${pageName.charAt(0).toUpperCase() + pageName.slice(1)}',
177
- description: '${description || `Learn more about ${pageName} on ${siteName}.`}',
178
- };
179
-
180
- export default function ${pageName.charAt(0).toUpperCase() + pageName.slice(1)}Page() {
181
- return (
182
- <main>
183
- <h1>${pageName.charAt(0).toUpperCase() + pageName.slice(1)}</h1>
184
- {/* Your content here */}
185
- </main>
186
- );
187
- }`,
188
- explanation: `Next.js page with metadata export. The title will be "${pageName} | ${siteName}" using the template.`,
189
- };
190
- }
191
-
192
- export function generateNextJsDynamicMetadata(): GeneratedCode {
193
- return {
194
- file: 'app/[slug]/page.tsx',
195
- code: `import type { Metadata } from 'next';
196
-
197
- interface PageProps {
198
- params: { slug: string };
199
- }
200
-
201
- // Generate metadata dynamically based on the slug
202
- export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
203
- // Fetch data based on slug (replace with your data fetching logic)
204
- const data = await fetchPageData(params.slug);
205
-
206
- return {
207
- title: data.title,
208
- description: data.description,
209
- openGraph: {
210
- title: data.title,
211
- description: data.description,
212
- images: data.image ? [{ url: data.image }] : [],
213
- },
214
- };
215
- }
216
-
217
- // Pre-generate static pages for known slugs (improves SEO)
218
- export async function generateStaticParams() {
219
- const pages = await fetchAllPageSlugs();
220
- return pages.map((slug) => ({ slug }));
221
- }
222
-
223
- async function fetchPageData(slug: string) {
224
- // Replace with your actual data fetching
225
- return {
226
- title: slug.replace(/-/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()),
227
- description: \`Learn about \${slug}\`,
228
- image: null,
229
- };
230
- }
231
-
232
- async function fetchAllPageSlugs() {
233
- // Replace with your actual data source
234
- return ['about', 'contact', 'pricing'];
235
- }
236
-
237
- export default function DynamicPage({ params }: PageProps) {
238
- return (
239
- <main>
240
- <h1>{params.slug}</h1>
241
- </main>
242
- );
243
- }`,
244
- explanation: 'Next.js dynamic route with generateMetadata and generateStaticParams for SEO-optimized dynamic pages.',
245
- };
246
- }
247
-
248
- export function generateNextJsRobots(siteUrl: string): GeneratedCode {
249
- return {
250
- file: 'app/robots.ts',
251
- code: `import type { MetadataRoute } from 'next';
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';
252
543
 
253
544
  export default function robots(): MetadataRoute.Robots {
545
+ const baseUrl = '${siteUrl}';
546
+
254
547
  return {
255
548
  rules: [
256
549
  {
257
550
  userAgent: '*',
258
551
  allow: '/',
259
- disallow: ['/api/', '/admin/', '/_next/'],
552
+ disallow: ['/api/', '/admin/', '/_next/', '/private/'],
553
+ },
554
+ {
555
+ userAgent: 'GPTBot',
556
+ allow: '/',
260
557
  },
261
558
  ],
262
- sitemap: '${siteUrl}/sitemap.xml',
559
+ sitemap: \`\${baseUrl}/sitemap.xml\`,
263
560
  };
264
561
  }`,
265
- explanation: 'Next.js App Router robots.txt generator. Automatically served at /robots.txt.',
266
- };
267
- }
268
-
269
- export function generateNextJsSitemap(siteUrl: string): GeneratedCode {
270
- return {
271
- file: 'app/sitemap.ts',
272
- code: `import type { MetadataRoute } from 'next';
562
+ explanation: 'Robots.txt with AI crawler support.',
563
+ },
564
+ {
565
+ file: 'app/sitemap.ts',
566
+ code: `import type { MetadataRoute } from 'next';
273
567
 
274
568
  export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
569
+ const baseUrl = '${siteUrl}';
570
+
275
571
  // Static pages
276
572
  const staticPages: MetadataRoute.Sitemap = [
277
573
  {
278
- url: '${siteUrl}',
574
+ url: baseUrl,
279
575
  lastModified: new Date(),
280
576
  changeFrequency: 'daily',
281
577
  priority: 1,
282
578
  },
283
579
  {
284
- url: '${siteUrl}/about',
580
+ url: \`\${baseUrl}/about\`,
285
581
  lastModified: new Date(),
286
582
  changeFrequency: 'monthly',
287
583
  priority: 0.8,
288
584
  },
289
585
  {
290
- url: '${siteUrl}/pricing',
586
+ url: \`\${baseUrl}/pricing\`,
291
587
  lastModified: new Date(),
292
588
  changeFrequency: 'weekly',
293
589
  priority: 0.9,
294
590
  },
591
+ {
592
+ url: \`\${baseUrl}/blog\`,
593
+ lastModified: new Date(),
594
+ changeFrequency: 'daily',
595
+ priority: 0.8,
596
+ },
295
597
  ];
296
598
 
297
- // Dynamic pages (fetch from your database/CMS)
298
- // const posts = await fetchAllPosts();
599
+ // Dynamic pages - fetch from your database/CMS
600
+ // const posts = await db.post.findMany({ select: { slug: true, updatedAt: true } });
299
601
  // const dynamicPages = posts.map((post) => ({
300
- // url: \`${siteUrl}/blog/\${post.slug}\`,
602
+ // url: \`\${baseUrl}/blog/\${post.slug}\`,
301
603
  // lastModified: post.updatedAt,
302
604
  // changeFrequency: 'weekly' as const,
303
605
  // priority: 0.7,
@@ -305,234 +607,646 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
305
607
 
306
608
  return [...staticPages];
307
609
  }`,
308
- explanation: 'Next.js App Router sitemap generator. Automatically served at /sitemap.xml.',
309
- };
310
- }
311
-
312
- // ============================================================================
313
- // NEXT.JS (Pages Router) - Head component
314
- // ============================================================================
610
+ explanation: 'Dynamic sitemap generator.',
611
+ },
612
+ {
613
+ file: 'lib/seo.ts',
614
+ code: `import type { Metadata } from 'next';
315
615
 
316
- export function generateNextJsPagesRouterHead(options: MetaFixOptions): GeneratedCode {
317
- const { siteName, siteUrl, title, description, image } = options;
318
-
319
- return {
320
- file: 'components/SEOHead.tsx',
321
- code: `import Head from 'next/head';
616
+ const baseUrl = '${siteUrl}';
617
+ const siteName = '${siteName}';
322
618
 
323
- interface SEOHeadProps {
324
- title?: string;
325
- description?: string;
619
+ interface PageSEOProps {
620
+ title: string;
621
+ description: string;
622
+ path?: string;
326
623
  image?: string;
327
- url?: string;
328
624
  type?: 'website' | 'article';
625
+ publishedTime?: string;
626
+ modifiedTime?: string;
627
+ authors?: string[];
628
+ tags?: string[];
629
+ noIndex?: boolean;
329
630
  }
330
631
 
331
- export function SEOHead({
332
- title = '${title || siteName}',
333
- description = '${description || `${siteName} - A compelling description.`}',
334
- image = '${image || `${siteUrl}/og-image.png`}',
335
- url = '${siteUrl}',
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,
336
641
  type = 'website',
337
- }: SEOHeadProps) {
338
- const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
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';
339
650
 
340
- return (
341
- <Head>
342
- <title>{fullTitle}</title>
343
- <meta name="description" content={description} />
344
- <link rel="canonical" href={url} />
345
-
346
- <meta property="og:type" content={type} />
347
- <meta property="og:url" content={url} />
348
- <meta property="og:title" content={fullTitle} />
349
- <meta property="og:description" content={description} />
350
- <meta property="og:image" content={image} />
351
- <meta property="og:site_name" content="${siteName}" />
352
-
353
- <meta name="twitter:card" content="summary_large_image" />
354
- <meta name="twitter:title" content={fullTitle} />
355
- <meta name="twitter:description" content={description} />
356
- <meta name="twitter:image" content={image} />
357
- </Head>
358
- );
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
+ };
359
776
  }`,
360
- explanation: 'Next.js Pages Router SEO component using next/head. Import and use on each page.',
777
+ explanation: 'SEO utility functions for generating metadata and JSON-LD.',
778
+ },
779
+ ],
361
780
  };
362
781
  }
363
782
 
364
783
  // ============================================================================
365
- // VUE.JS / NUXT - useHead composable
784
+ // NUXT 3 - Comprehensive useHead composable
366
785
  // ============================================================================
367
786
 
368
787
  export function generateNuxtSEOHead(options: MetaFixOptions): GeneratedCode {
369
- const { siteName, siteUrl, title, description, image } = options;
788
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
370
789
 
371
790
  return {
372
791
  file: 'composables/useSEO.ts',
373
- code: `export function useSEO(options: {
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 {
374
805
  title?: string;
375
806
  description?: string;
376
807
  image?: string;
377
808
  url?: string;
378
- type?: 'website' | 'article';
379
- } = {}) {
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 = {}) {
380
833
  const route = useRoute();
381
- const config = useRuntimeConfig();
382
834
 
383
- const defaults = {
384
- title: '${title || siteName}',
385
- description: '${description || `${siteName} - A compelling description.`}',
386
- image: '${image || `${siteUrl}/og-image.png`}',
387
- url: \`${siteUrl}\${route.path}\`,
388
- type: 'website' as const,
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,
389
906
  };
390
907
 
391
- const meta = { ...defaults, ...options };
392
- const fullTitle = meta.title.includes('${siteName}')
393
- ? meta.title
394
- : \`\${meta.title} | ${siteName}\`;
908
+ const jsonLd = schema
909
+ ? Array.isArray(schema)
910
+ ? [defaultSchema, ...schema]
911
+ : [defaultSchema, schema]
912
+ : [defaultSchema];
395
913
 
396
914
  useHead({
397
915
  title: fullTitle,
398
- meta: [
399
- { name: 'description', content: meta.description },
400
- // Open Graph
401
- { property: 'og:type', content: meta.type },
402
- { property: 'og:url', content: meta.url },
403
- { property: 'og:title', content: fullTitle },
404
- { property: 'og:description', content: meta.description },
405
- { property: 'og:image', content: meta.image },
406
- { property: 'og:site_name', content: '${siteName}' },
407
- // Twitter
408
- { name: 'twitter:card', content: 'summary_large_image' },
409
- { name: 'twitter:title', content: fullTitle },
410
- { name: 'twitter:description', content: meta.description },
411
- { name: 'twitter:image', content: meta.image },
412
- ],
916
+ meta,
413
917
  link: [
414
- { rel: 'canonical', href: meta.url },
918
+ { rel: 'canonical', href: pageUrl },
919
+ ],
920
+ script: [
921
+ {
922
+ type: 'application/ld+json',
923
+ innerHTML: JSON.stringify(jsonLd),
924
+ },
415
925
  ],
416
926
  });
417
- }`,
418
- explanation: 'Nuxt 3 SEO composable using useHead(). Call useSEO() in any page to set meta tags.',
419
- };
420
927
  }
421
928
 
422
- export function generateNuxtPageExample(): GeneratedCode {
423
- return {
424
- file: 'pages/about.vue',
425
- code: `<script setup lang="ts">
426
- useSEO({
427
- title: 'About Us',
428
- description: 'Learn more about our company and mission.',
429
- });
430
- </script>
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
+ // }));
431
1032
 
432
- <template>
433
- <main>
434
- <h1>About Us</h1>
435
- <!-- Your content here -->
436
- </main>
437
- </template>`,
438
- explanation: 'Example Nuxt page using the useSEO composable.',
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
+ ],
439
1055
  };
440
1056
  }
441
1057
 
442
1058
  // ============================================================================
443
- // VUE.JS (without Nuxt) - vue-meta or @unhead/vue
1059
+ // VUE.JS (without Nuxt) - @unhead/vue
444
1060
  // ============================================================================
445
1061
 
446
1062
  export function generateVueSEOHead(options: MetaFixOptions): GeneratedCode {
447
- const { siteName, siteUrl, title, description, image } = options;
1063
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
448
1064
 
449
1065
  return {
450
1066
  file: 'src/composables/useSEO.ts',
451
- code: `import { useHead } from '@unhead/vue';
452
- import { computed, ref } from 'vue';
1067
+ code: `import { useHead, useServerHead } from '@unhead/vue';
1068
+ import { computed, unref, MaybeRef } from 'vue';
1069
+ import { useRoute } from 'vue-router';
453
1070
 
454
1071
  interface SEOOptions {
455
- title?: string;
456
- description?: string;
457
- image?: string;
458
- url?: string;
1072
+ title?: MaybeRef<string>;
1073
+ description?: MaybeRef<string>;
1074
+ image?: MaybeRef<string>;
459
1075
  type?: 'website' | 'article';
1076
+ noIndex?: boolean;
1077
+ schema?: Record<string, unknown>;
460
1078
  }
461
1079
 
462
- export function useSEO(options: SEOOptions = {}) {
463
- const defaults = {
464
- title: '${title || siteName}',
465
- description: '${description || `${siteName} - A compelling description.`}',
466
- image: '${image || `${siteUrl}/og-image.png`}',
467
- url: typeof window !== 'undefined' ? window.location.href : '${siteUrl}',
468
- type: 'website' as const,
469
- };
470
-
471
- const meta = { ...defaults, ...options };
472
- const fullTitle = computed(() =>
473
- meta.title.includes('${siteName}') ? meta.title : \`\${meta.title} | ${siteName}\`
474
- );
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 || ''}';
475
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
+
476
1101
  useHead({
477
- title: fullTitle,
1102
+ title,
478
1103
  meta: [
479
- { name: 'description', content: meta.description },
480
- { property: 'og:type', content: meta.type },
481
- { property: 'og:url', content: meta.url },
482
- { property: 'og:title', content: fullTitle.value },
483
- { property: 'og:description', content: meta.description },
484
- { property: 'og:image', content: meta.image },
485
- { property: 'og:site_name', content: '${siteName}' },
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
486
1116
  { name: 'twitter:card', content: 'summary_large_image' },
487
- { name: 'twitter:title', content: fullTitle.value },
488
- { name: 'twitter:description', content: meta.description },
489
- { name: 'twitter:image', content: meta.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
+ ] : []),
490
1124
  ],
491
1125
  link: [
492
- { rel: 'canonical', href: meta.url },
1126
+ { rel: 'canonical', href: url },
493
1127
  ],
1128
+ script: options.schema ? [
1129
+ { type: 'application/ld+json', innerHTML: JSON.stringify(options.schema) },
1130
+ ] : [],
494
1131
  });
495
1132
  }`,
496
- explanation: 'Vue 3 SEO composable using @unhead/vue. Install with: npm install @unhead/vue',
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`,
497
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
+ ],
498
1162
  };
499
1163
  }
500
1164
 
501
1165
  // ============================================================================
502
- // ASTRO - Frontmatter and BaseHead component
1166
+ // ASTRO - Comprehensive BaseHead component
503
1167
  // ============================================================================
504
1168
 
505
1169
  export function generateAstroBaseHead(options: MetaFixOptions): GeneratedCode {
506
- const { siteName, siteUrl, title, description, image } = options;
1170
+ const { siteName, siteUrl, description, image, twitterHandle, locale } = options;
507
1171
 
508
1172
  return {
509
1173
  file: 'src/components/BaseHead.astro',
510
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
+
511
1187
  interface Props {
512
1188
  title?: string;
513
1189
  description?: string;
514
1190
  image?: string;
515
1191
  type?: 'website' | 'article';
1192
+ publishedTime?: string;
1193
+ modifiedTime?: string;
1194
+ author?: string;
1195
+ tags?: string[];
1196
+ noIndex?: boolean;
1197
+ schema?: Record<string, unknown>;
516
1198
  }
517
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
+
518
1207
  const {
519
- title = '${title || siteName}',
520
- description = '${description || `${siteName} - A compelling description.`}',
521
- image = '${image || '/og-image.png'}',
1208
+ title,
1209
+ description = DEFAULT_DESCRIPTION,
1210
+ image = DEFAULT_IMAGE,
522
1211
  type = 'website',
1212
+ publishedTime,
1213
+ modifiedTime,
1214
+ author,
1215
+ tags,
1216
+ noIndex = false,
1217
+ schema,
523
1218
  } = Astro.props;
524
1219
 
525
- const canonicalURL = new URL(Astro.url.pathname, Astro.site);
526
- const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
527
- const imageURL = new URL(image, Astro.site);
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];
528
1236
  ---
529
1237
 
530
1238
  <!-- Global Metadata -->
531
1239
  <meta charset="utf-8" />
532
- <meta name="viewport" content="width=device-width, initial-scale=1" />
533
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
1240
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
534
1241
  <meta name="generator" content={Astro.generator} />
535
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
+
536
1250
  <!-- Canonical URL -->
537
1251
  <link rel="canonical" href={canonicalURL} />
538
1252
 
@@ -540,6 +1254,11 @@ const imageURL = new URL(image, Astro.site);
540
1254
  <title>{fullTitle}</title>
541
1255
  <meta name="title" content={fullTitle} />
542
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)" />
543
1262
 
544
1263
  <!-- Open Graph / Facebook -->
545
1264
  <meta property="og:type" content={type} />
@@ -547,156 +1266,461 @@ const imageURL = new URL(image, Astro.site);
547
1266
  <meta property="og:title" content={fullTitle} />
548
1267
  <meta property="og:description" content={description} />
549
1268
  <meta property="og:image" content={imageURL} />
550
- <meta property="og:site_name" content="${siteName}" />
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
+ ))}
551
1287
 
552
1288
  <!-- Twitter -->
553
1289
  <meta name="twitter:card" content="summary_large_image" />
554
1290
  <meta name="twitter:url" content={canonicalURL} />
555
1291
  <meta name="twitter:title" content={fullTitle} />
556
1292
  <meta name="twitter:description" content={description} />
557
- <meta name="twitter:image" content={imageURL} />`,
558
- explanation: 'Astro BaseHead component. Import in your layout: <BaseHead title="Page Title" description="..." />',
559
- };
560
- }
561
-
562
- export function generateAstroLayout(siteName: string): GeneratedCode {
563
- return {
564
- file: 'src/layouts/BaseLayout.astro',
565
- code: `---
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: `---
566
1317
  import BaseHead from '../components/BaseHead.astro';
567
1318
 
568
1319
  interface Props {
569
1320
  title?: string;
570
1321
  description?: string;
571
1322
  image?: string;
1323
+ type?: 'website' | 'article';
1324
+ schema?: Record<string, unknown>;
572
1325
  }
573
1326
 
574
- const { title, description, image } = Astro.props;
1327
+ const { title, description, image, type, schema } = Astro.props;
575
1328
  ---
576
1329
 
577
1330
  <!DOCTYPE html>
578
- <html lang="en">
1331
+ <html lang="${(locale || 'en_US').split('_')[0]}">
579
1332
  <head>
580
- <BaseHead title={title} description={description} image={image} />
1333
+ <BaseHead
1334
+ title={title}
1335
+ description={description}
1336
+ image={image}
1337
+ type={type}
1338
+ schema={schema}
1339
+ />
581
1340
  </head>
582
1341
  <body>
583
- <main>
584
- <slot />
585
- </main>
1342
+ <slot />
586
1343
  </body>
587
1344
  </html>`,
588
- explanation: 'Astro layout using BaseHead component. Use in pages with frontmatter.',
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
+ ],
589
1360
  };
590
1361
  }
591
1362
 
592
1363
  // ============================================================================
593
- // SVELTEKIT - svelte:head
1364
+ // SVELTEKIT - Comprehensive svelte:head
594
1365
  // ============================================================================
595
1366
 
596
1367
  export function generateSvelteKitSEOHead(options: MetaFixOptions): GeneratedCode {
597
- const { siteName, siteUrl, title, description, image } = options;
1368
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
598
1369
 
599
1370
  return {
600
1371
  file: 'src/lib/components/SEOHead.svelte',
601
1372
  code: `<script lang="ts">
602
1373
  import { page } from '$app/stores';
603
1374
 
604
- export let title = '${title || siteName}';
605
- export let description = '${description || `${siteName} - A compelling description.`}';
606
- export let image = '${image || `${siteUrl}/og-image.png`}';
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`}';
607
1378
  export let type: 'website' | 'article' = 'website';
608
-
609
- $: fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
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;
610
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];
611
1405
  </script>
612
1406
 
613
1407
  <svelte:head>
1408
+ <!-- Primary Meta Tags -->
614
1409
  <title>{fullTitle}</title>
1410
+ <meta name="title" content={fullTitle} />
615
1411
  <meta name="description" content={description} />
1412
+ <meta name="robots" content={robotsContent} />
616
1413
  <link rel="canonical" href={canonicalUrl} />
617
-
1414
+
1415
+ <!-- Open Graph / Facebook -->
618
1416
  <meta property="og:type" content={type} />
619
1417
  <meta property="og:url" content={canonicalUrl} />
620
1418
  <meta property="og:title" content={fullTitle} />
621
1419
  <meta property="og:description" content={description} />
622
- <meta property="og:image" content={image} />
623
- <meta property="og:site_name" content="${siteName}" />
624
-
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 -->
625
1441
  <meta name="twitter:card" content="summary_large_image" />
626
1442
  <meta name="twitter:title" content={fullTitle} />
627
1443
  <meta name="twitter:description" content={description} />
628
- <meta name="twitter:image" content={image} />
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>\`}
629
1452
  </svelte:head>`,
630
- explanation: 'SvelteKit SEO component using svelte:head. Import and use in +page.svelte files.',
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
+ ],
631
1484
  };
632
1485
  }
633
1486
 
634
1487
  // ============================================================================
635
- // ANGULAR - Meta and Title services
1488
+ // ANGULAR - Comprehensive SEO Service
636
1489
  // ============================================================================
637
1490
 
638
1491
  export function generateAngularSEOService(options: MetaFixOptions): GeneratedCode {
639
- const { siteName, siteUrl, title, description, image } = options;
1492
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
640
1493
 
641
1494
  return {
642
- file: 'src/app/services/seo.service.ts',
643
- code: `import { Injectable } from '@angular/core';
1495
+ file: 'src/app/core/services/seo.service.ts',
1496
+ code: `import { Injectable, Inject } from '@angular/core';
644
1497
  import { Meta, Title } from '@angular/platform-browser';
645
- import { Router } from '@angular/router';
1498
+ import { Router, NavigationEnd } from '@angular/router';
1499
+ import { DOCUMENT } from '@angular/common';
1500
+ import { filter } from 'rxjs/operators';
646
1501
 
647
1502
  interface SEOConfig {
648
1503
  title?: string;
649
1504
  description?: string;
650
1505
  image?: string;
651
1506
  type?: 'website' | 'article';
1507
+ publishedTime?: string;
1508
+ modifiedTime?: string;
1509
+ author?: string;
1510
+ tags?: string[];
1511
+ noIndex?: boolean;
1512
+ schema?: Record<string, unknown>;
652
1513
  }
653
1514
 
654
1515
  @Injectable({
655
1516
  providedIn: 'root'
656
1517
  })
657
1518
  export class SEOService {
658
- private siteName = '${siteName}';
659
- private siteUrl = '${siteUrl}';
660
- private defaultDescription = '${description || `${siteName} - A compelling description.`}';
661
- private defaultImage = '${image || `${siteUrl}/og-image.png`}';
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 || ''}';
662
1524
 
663
1525
  constructor(
664
1526
  private meta: Meta,
665
1527
  private titleService: Title,
666
- private router: Router
667
- ) {}
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
+ }
668
1538
 
1539
+ /**
1540
+ * Update all SEO meta tags
1541
+ */
669
1542
  updateMeta(config: SEOConfig = {}): void {
670
- const title = config.title || this.siteName;
671
- const fullTitle = title.includes(this.siteName) ? title : \`\${title} | \${this.siteName}\`;
672
- const description = config.description || this.defaultDescription;
673
- const image = config.image || this.defaultImage;
674
- const url = this.siteUrl + this.router.url;
675
- const type = config.type || 'website';
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';
676
1562
 
677
1563
  // Title
678
1564
  this.titleService.setTitle(fullTitle);
679
1565
 
680
- // Primary Meta
681
- this.meta.updateTag({ name: 'description', content: description });
682
- this.meta.updateTag({ rel: 'canonical', href: url });
1566
+ // Primary Meta Tags
1567
+ this.setMetaTag('description', description);
1568
+ this.setMetaTag('robots', robotsContent);
683
1569
 
684
1570
  // Open Graph
685
- this.meta.updateTag({ property: 'og:type', content: type });
686
- this.meta.updateTag({ property: 'og:url', content: url });
687
- this.meta.updateTag({ property: 'og:title', content: fullTitle });
688
- this.meta.updateTag({ property: 'og:description', content: description });
689
- this.meta.updateTag({ property: 'og:image', content: image });
690
- this.meta.updateTag({ property: 'og:site_name', content: this.siteName });
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
+ }
691
1587
 
692
1588
  // Twitter
693
- this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
694
- this.meta.updateTag({ name: 'twitter:title', content: fullTitle });
695
- this.meta.updateTag({ name: 'twitter:description', content: description });
696
- this.meta.updateTag({ name: 'twitter:image', content: image });
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
+ };
697
1713
  }
698
1714
  }`,
699
- explanation: 'Angular SEO service using Meta and Title services. Inject and call updateMeta() in components.',
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()`,
700
1724
  };
701
1725
  }
702
1726
 
@@ -741,3 +1765,105 @@ export function getFrameworkSpecificFix(
741
1765
  // Default: React with react-helmet-async
742
1766
  return generateReactSEOHead(options);
743
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
+ }