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