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