@rankcli/agent-runtime 0.0.5 → 0.0.7

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,743 @@
1
+ /**
2
+ * Framework-Specific SEO Fix Generators
3
+ *
4
+ * Generates proper code snippets for each framework's meta tag patterns.
5
+ */
6
+
7
+ import type { FrameworkInfo } from '../types.js';
8
+
9
+ export interface MetaFixOptions {
10
+ siteName: string;
11
+ siteUrl: string;
12
+ title?: string;
13
+ description?: string;
14
+ image?: string;
15
+ }
16
+
17
+ export interface GeneratedCode {
18
+ file: string;
19
+ code: string;
20
+ explanation: string;
21
+ installCommands?: string[];
22
+ imports?: string[];
23
+ }
24
+
25
+ // ============================================================================
26
+ // REACT (Vite/CRA) - Client-side with react-helmet-async
27
+ // ============================================================================
28
+
29
+ export function generateReactSEOHead(options: MetaFixOptions): GeneratedCode {
30
+ const { siteName, siteUrl, title, description, image } = options;
31
+
32
+ return {
33
+ file: 'src/components/SEOHead.tsx',
34
+ code: `import { Helmet } from 'react-helmet-async';
35
+
36
+ interface SEOHeadProps {
37
+ title?: string;
38
+ description?: string;
39
+ image?: string;
40
+ url?: string;
41
+ type?: 'website' | 'article';
42
+ }
43
+
44
+ export function SEOHead({
45
+ title = '${title || `${siteName} - Your tagline here`}',
46
+ 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}',
49
+ type = 'website',
50
+ }: SEOHeadProps) {
51
+ const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
52
+
53
+ return (
54
+ <Helmet>
55
+ {/* Primary Meta Tags */}
56
+ <title>{fullTitle}</title>
57
+ <meta name="description" content={description} />
58
+ <link rel="canonical" href={url} />
59
+
60
+ {/* Open Graph / Facebook */}
61
+ <meta property="og:type" content={type} />
62
+ <meta property="og:url" content={url} />
63
+ <meta property="og:title" content={fullTitle} />
64
+ <meta property="og:description" content={description} />
65
+ <meta property="og:image" content={image} />
66
+ <meta property="og:site_name" content="${siteName}" />
67
+
68
+ {/* Twitter */}
69
+ <meta name="twitter:card" content="summary_large_image" />
70
+ <meta name="twitter:url" content={url} />
71
+ <meta name="twitter:title" content={fullTitle} />
72
+ <meta name="twitter:description" content={description} />
73
+ <meta name="twitter:image" content={image} />
74
+ </Helmet>
75
+ );
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
+ }
82
+
83
+ export function generateReactAppWrapper(): GeneratedCode {
84
+ return {
85
+ file: 'src/main.tsx',
86
+ code: `import React from 'react';
87
+ import ReactDOM from 'react-dom/client';
88
+ import { HelmetProvider } from 'react-helmet-async';
89
+ import App from './App';
90
+ import './index.css';
91
+
92
+ ReactDOM.createRoot(document.getElementById('root')!).render(
93
+ <React.StrictMode>
94
+ <HelmetProvider>
95
+ <App />
96
+ </HelmetProvider>
97
+ </React.StrictMode>,
98
+ );`,
99
+ explanation: 'Updated main.tsx with HelmetProvider wrapper for react-helmet-async.',
100
+ };
101
+ }
102
+
103
+ // ============================================================================
104
+ // NEXT.JS (App Router) - Server-side metadata export
105
+ // ============================================================================
106
+
107
+ export function generateNextJsAppRouterMetadata(options: MetaFixOptions): GeneratedCode {
108
+ const { siteName, siteUrl, title, description, image } = options;
109
+
110
+ return {
111
+ file: 'app/layout.tsx',
112
+ code: `import type { Metadata } from 'next';
113
+ import { Inter } from 'next/font/google';
114
+ import './globals.css';
115
+
116
+ const inter = Inter({ subsets: ['latin'] });
117
+
118
+ export const metadata: Metadata = {
119
+ metadataBase: new URL('${siteUrl}'),
120
+ title: {
121
+ default: '${title || siteName}',
122
+ template: \`%s | ${siteName}\`,
123
+ },
124
+ description: '${description || `${siteName} - A compelling description of your product or service.`}',
125
+ openGraph: {
126
+ type: 'website',
127
+ locale: 'en_US',
128
+ url: '${siteUrl}',
129
+ siteName: '${siteName}',
130
+ title: '${title || siteName}',
131
+ description: '${description || `${siteName} - A compelling description.`}',
132
+ images: [
133
+ {
134
+ url: '${image || '/og-image.png'}',
135
+ width: 1200,
136
+ height: 630,
137
+ alt: '${siteName}',
138
+ },
139
+ ],
140
+ },
141
+ twitter: {
142
+ card: 'summary_large_image',
143
+ title: '${title || siteName}',
144
+ description: '${description || `${siteName} - A compelling description.`}',
145
+ images: ['${image || '/og-image.png'}'],
146
+ },
147
+ robots: {
148
+ index: true,
149
+ follow: true,
150
+ },
151
+ };
152
+
153
+ export default function RootLayout({
154
+ children,
155
+ }: {
156
+ children: React.ReactNode;
157
+ }) {
158
+ return (
159
+ <html lang="en">
160
+ <body className={inter.className}>{children}</body>
161
+ </html>
162
+ );
163
+ }`,
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';
252
+
253
+ export default function robots(): MetadataRoute.Robots {
254
+ return {
255
+ rules: [
256
+ {
257
+ userAgent: '*',
258
+ allow: '/',
259
+ disallow: ['/api/', '/admin/', '/_next/'],
260
+ },
261
+ ],
262
+ sitemap: '${siteUrl}/sitemap.xml',
263
+ };
264
+ }`,
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';
273
+
274
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
275
+ // Static pages
276
+ const staticPages: MetadataRoute.Sitemap = [
277
+ {
278
+ url: '${siteUrl}',
279
+ lastModified: new Date(),
280
+ changeFrequency: 'daily',
281
+ priority: 1,
282
+ },
283
+ {
284
+ url: '${siteUrl}/about',
285
+ lastModified: new Date(),
286
+ changeFrequency: 'monthly',
287
+ priority: 0.8,
288
+ },
289
+ {
290
+ url: '${siteUrl}/pricing',
291
+ lastModified: new Date(),
292
+ changeFrequency: 'weekly',
293
+ priority: 0.9,
294
+ },
295
+ ];
296
+
297
+ // Dynamic pages (fetch from your database/CMS)
298
+ // const posts = await fetchAllPosts();
299
+ // const dynamicPages = posts.map((post) => ({
300
+ // url: \`${siteUrl}/blog/\${post.slug}\`,
301
+ // lastModified: post.updatedAt,
302
+ // changeFrequency: 'weekly' as const,
303
+ // priority: 0.7,
304
+ // }));
305
+
306
+ return [...staticPages];
307
+ }`,
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
+ // ============================================================================
315
+
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';
322
+
323
+ interface SEOHeadProps {
324
+ title?: string;
325
+ description?: string;
326
+ image?: string;
327
+ url?: string;
328
+ type?: 'website' | 'article';
329
+ }
330
+
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}',
336
+ type = 'website',
337
+ }: SEOHeadProps) {
338
+ const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
339
+
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
+ );
359
+ }`,
360
+ explanation: 'Next.js Pages Router SEO component using next/head. Import and use on each page.',
361
+ };
362
+ }
363
+
364
+ // ============================================================================
365
+ // VUE.JS / NUXT - useHead composable
366
+ // ============================================================================
367
+
368
+ export function generateNuxtSEOHead(options: MetaFixOptions): GeneratedCode {
369
+ const { siteName, siteUrl, title, description, image } = options;
370
+
371
+ return {
372
+ file: 'composables/useSEO.ts',
373
+ code: `export function useSEO(options: {
374
+ title?: string;
375
+ description?: string;
376
+ image?: string;
377
+ url?: string;
378
+ type?: 'website' | 'article';
379
+ } = {}) {
380
+ const route = useRoute();
381
+ const config = useRuntimeConfig();
382
+
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,
389
+ };
390
+
391
+ const meta = { ...defaults, ...options };
392
+ const fullTitle = meta.title.includes('${siteName}')
393
+ ? meta.title
394
+ : \`\${meta.title} | ${siteName}\`;
395
+
396
+ useHead({
397
+ 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
+ ],
413
+ link: [
414
+ { rel: 'canonical', href: meta.url },
415
+ ],
416
+ });
417
+ }`,
418
+ explanation: 'Nuxt 3 SEO composable using useHead(). Call useSEO() in any page to set meta tags.',
419
+ };
420
+ }
421
+
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>
431
+
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.',
439
+ };
440
+ }
441
+
442
+ // ============================================================================
443
+ // VUE.JS (without Nuxt) - vue-meta or @unhead/vue
444
+ // ============================================================================
445
+
446
+ export function generateVueSEOHead(options: MetaFixOptions): GeneratedCode {
447
+ const { siteName, siteUrl, title, description, image } = options;
448
+
449
+ return {
450
+ file: 'src/composables/useSEO.ts',
451
+ code: `import { useHead } from '@unhead/vue';
452
+ import { computed, ref } from 'vue';
453
+
454
+ interface SEOOptions {
455
+ title?: string;
456
+ description?: string;
457
+ image?: string;
458
+ url?: string;
459
+ type?: 'website' | 'article';
460
+ }
461
+
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
+ );
475
+
476
+ useHead({
477
+ title: fullTitle,
478
+ 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}' },
486
+ { 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 },
490
+ ],
491
+ link: [
492
+ { rel: 'canonical', href: meta.url },
493
+ ],
494
+ });
495
+ }`,
496
+ explanation: 'Vue 3 SEO composable using @unhead/vue. Install with: npm install @unhead/vue',
497
+ installCommands: ['npm install @unhead/vue'],
498
+ };
499
+ }
500
+
501
+ // ============================================================================
502
+ // ASTRO - Frontmatter and BaseHead component
503
+ // ============================================================================
504
+
505
+ export function generateAstroBaseHead(options: MetaFixOptions): GeneratedCode {
506
+ const { siteName, siteUrl, title, description, image } = options;
507
+
508
+ return {
509
+ file: 'src/components/BaseHead.astro',
510
+ code: `---
511
+ interface Props {
512
+ title?: string;
513
+ description?: string;
514
+ image?: string;
515
+ type?: 'website' | 'article';
516
+ }
517
+
518
+ const {
519
+ title = '${title || siteName}',
520
+ description = '${description || `${siteName} - A compelling description.`}',
521
+ image = '${image || '/og-image.png'}',
522
+ type = 'website',
523
+ } = Astro.props;
524
+
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);
528
+ ---
529
+
530
+ <!-- Global Metadata -->
531
+ <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" />
534
+ <meta name="generator" content={Astro.generator} />
535
+
536
+ <!-- Canonical URL -->
537
+ <link rel="canonical" href={canonicalURL} />
538
+
539
+ <!-- Primary Meta Tags -->
540
+ <title>{fullTitle}</title>
541
+ <meta name="title" content={fullTitle} />
542
+ <meta name="description" content={description} />
543
+
544
+ <!-- Open Graph / Facebook -->
545
+ <meta property="og:type" content={type} />
546
+ <meta property="og:url" content={canonicalURL} />
547
+ <meta property="og:title" content={fullTitle} />
548
+ <meta property="og:description" content={description} />
549
+ <meta property="og:image" content={imageURL} />
550
+ <meta property="og:site_name" content="${siteName}" />
551
+
552
+ <!-- Twitter -->
553
+ <meta name="twitter:card" content="summary_large_image" />
554
+ <meta name="twitter:url" content={canonicalURL} />
555
+ <meta name="twitter:title" content={fullTitle} />
556
+ <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: `---
566
+ import BaseHead from '../components/BaseHead.astro';
567
+
568
+ interface Props {
569
+ title?: string;
570
+ description?: string;
571
+ image?: string;
572
+ }
573
+
574
+ const { title, description, image } = Astro.props;
575
+ ---
576
+
577
+ <!DOCTYPE html>
578
+ <html lang="en">
579
+ <head>
580
+ <BaseHead title={title} description={description} image={image} />
581
+ </head>
582
+ <body>
583
+ <main>
584
+ <slot />
585
+ </main>
586
+ </body>
587
+ </html>`,
588
+ explanation: 'Astro layout using BaseHead component. Use in pages with frontmatter.',
589
+ };
590
+ }
591
+
592
+ // ============================================================================
593
+ // SVELTEKIT - svelte:head
594
+ // ============================================================================
595
+
596
+ export function generateSvelteKitSEOHead(options: MetaFixOptions): GeneratedCode {
597
+ const { siteName, siteUrl, title, description, image } = options;
598
+
599
+ return {
600
+ file: 'src/lib/components/SEOHead.svelte',
601
+ code: `<script lang="ts">
602
+ import { page } from '$app/stores';
603
+
604
+ export let title = '${title || siteName}';
605
+ export let description = '${description || `${siteName} - A compelling description.`}';
606
+ export let image = '${image || `${siteUrl}/og-image.png`}';
607
+ export let type: 'website' | 'article' = 'website';
608
+
609
+ $: fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
610
+ $: canonicalUrl = $page.url.href;
611
+ </script>
612
+
613
+ <svelte:head>
614
+ <title>{fullTitle}</title>
615
+ <meta name="description" content={description} />
616
+ <link rel="canonical" href={canonicalUrl} />
617
+
618
+ <meta property="og:type" content={type} />
619
+ <meta property="og:url" content={canonicalUrl} />
620
+ <meta property="og:title" content={fullTitle} />
621
+ <meta property="og:description" content={description} />
622
+ <meta property="og:image" content={image} />
623
+ <meta property="og:site_name" content="${siteName}" />
624
+
625
+ <meta name="twitter:card" content="summary_large_image" />
626
+ <meta name="twitter:title" content={fullTitle} />
627
+ <meta name="twitter:description" content={description} />
628
+ <meta name="twitter:image" content={image} />
629
+ </svelte:head>`,
630
+ explanation: 'SvelteKit SEO component using svelte:head. Import and use in +page.svelte files.',
631
+ };
632
+ }
633
+
634
+ // ============================================================================
635
+ // ANGULAR - Meta and Title services
636
+ // ============================================================================
637
+
638
+ export function generateAngularSEOService(options: MetaFixOptions): GeneratedCode {
639
+ const { siteName, siteUrl, title, description, image } = options;
640
+
641
+ return {
642
+ file: 'src/app/services/seo.service.ts',
643
+ code: `import { Injectable } from '@angular/core';
644
+ import { Meta, Title } from '@angular/platform-browser';
645
+ import { Router } from '@angular/router';
646
+
647
+ interface SEOConfig {
648
+ title?: string;
649
+ description?: string;
650
+ image?: string;
651
+ type?: 'website' | 'article';
652
+ }
653
+
654
+ @Injectable({
655
+ providedIn: 'root'
656
+ })
657
+ 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`}';
662
+
663
+ constructor(
664
+ private meta: Meta,
665
+ private titleService: Title,
666
+ private router: Router
667
+ ) {}
668
+
669
+ 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';
676
+
677
+ // Title
678
+ this.titleService.setTitle(fullTitle);
679
+
680
+ // Primary Meta
681
+ this.meta.updateTag({ name: 'description', content: description });
682
+ this.meta.updateTag({ rel: 'canonical', href: url });
683
+
684
+ // 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 });
691
+
692
+ // 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 });
697
+ }
698
+ }`,
699
+ explanation: 'Angular SEO service using Meta and Title services. Inject and call updateMeta() in components.',
700
+ };
701
+ }
702
+
703
+ // ============================================================================
704
+ // Helper: Get framework-specific fix based on detected framework
705
+ // ============================================================================
706
+
707
+ export function getFrameworkSpecificFix(
708
+ framework: FrameworkInfo,
709
+ options: MetaFixOptions
710
+ ): GeneratedCode {
711
+ const name = framework.name.toLowerCase();
712
+
713
+ if (name.includes('next')) {
714
+ if (framework.router === 'app') {
715
+ return generateNextJsAppRouterMetadata(options);
716
+ } else {
717
+ return generateNextJsPagesRouterHead(options);
718
+ }
719
+ }
720
+
721
+ if (name.includes('nuxt')) {
722
+ return generateNuxtSEOHead(options);
723
+ }
724
+
725
+ if (name.includes('vue')) {
726
+ return generateVueSEOHead(options);
727
+ }
728
+
729
+ if (name.includes('astro')) {
730
+ return generateAstroBaseHead(options);
731
+ }
732
+
733
+ if (name.includes('svelte')) {
734
+ return generateSvelteKitSEOHead(options);
735
+ }
736
+
737
+ if (name.includes('angular')) {
738
+ return generateAngularSEOService(options);
739
+ }
740
+
741
+ // Default: React with react-helmet-async
742
+ return generateReactSEOHead(options);
743
+ }