@masters-ws/react-seo 1.4.4 → 1.5.1
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/README.md +308 -1482
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +213 -0
- package/dist/index.mjs +212 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,1482 +1,308 @@
|
|
|
1
|
-
[![
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- ✅
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"https://
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
breadcrumbs: [
|
|
310
|
-
{ name: "Home", item: "https://techblog.com" },
|
|
311
|
-
{ name: article.category.name, item: `https://techblog.com/category/${article.category.slug}` },
|
|
312
|
-
{ name: article.title, item: `https://techblog.com/blog/${params.slug}` },
|
|
313
|
-
],
|
|
314
|
-
}, siteConfig);
|
|
315
|
-
|
|
316
|
-
return metadata;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export default async function ArticlePage({ params }: { params: { slug: string } }) {
|
|
320
|
-
const article = await fetchArticle(params.slug);
|
|
321
|
-
const { schemas } = generateArticleMetadata({ /* same data as above */ }, siteConfig);
|
|
322
|
-
|
|
323
|
-
return (
|
|
324
|
-
<>
|
|
325
|
-
<JsonLd schema={schemas} graph />
|
|
326
|
-
<article>
|
|
327
|
-
<h1>{article.title}</h1>
|
|
328
|
-
<p>By {article.author.name} · {article.reading_time} min read</p>
|
|
329
|
-
<div dangerouslySetInnerHTML={{ __html: article.content }} />
|
|
330
|
-
</article>
|
|
331
|
-
</>
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
**What Google sees:**
|
|
337
|
-
```html
|
|
338
|
-
<title>How to Master SEO in Next.js | Tech Blog</title>
|
|
339
|
-
<meta name="description" content="A comprehensive guide to..." />
|
|
340
|
-
<meta property="og:type" content="article" />
|
|
341
|
-
<meta property="article:published_time" content="2024-06-15T10:00:00Z" />
|
|
342
|
-
<meta property="article:author" content="John Doe" />
|
|
343
|
-
<meta property="article:section" content="JavaScript" />
|
|
344
|
-
<meta property="article:tag" content="React,Next.js,SEO" />
|
|
345
|
-
<script type="application/ld+json">
|
|
346
|
-
{
|
|
347
|
-
"@context": "https://schema.org",
|
|
348
|
-
"@graph": [
|
|
349
|
-
{ "@type": "NewsArticle", "headline": "...", "author": { "@type": "Person", "name": "..." } },
|
|
350
|
-
{ "@type": "BreadcrumbList", "itemListElement": [...] },
|
|
351
|
-
{ "@type": "Organization", ... },
|
|
352
|
-
{ "@type": "WebSite", "potentialAction": { "@type": "SearchAction", ... } }
|
|
353
|
-
]
|
|
354
|
-
}
|
|
355
|
-
</script>
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
### Blog Category Page (with Pagination)
|
|
359
|
-
|
|
360
|
-
```tsx
|
|
361
|
-
// app/category/[slug]/page.tsx — Server Component
|
|
362
|
-
import { generateCategoryMetadata, JsonLd } from '@masters-ws/react-seo/core';
|
|
363
|
-
|
|
364
|
-
export async function generateMetadata({ params, searchParams }) {
|
|
365
|
-
const page = Number(searchParams.page) || 1;
|
|
366
|
-
const category = await fetchCategory(params.slug);
|
|
367
|
-
|
|
368
|
-
const { metadata } = generateCategoryMetadata({
|
|
369
|
-
name: category.name,
|
|
370
|
-
description: `All articles about ${category.name}`,
|
|
371
|
-
url: `https://techblog.com/category/${params.slug}`,
|
|
372
|
-
image: category.cover_image,
|
|
373
|
-
page,
|
|
374
|
-
totalPages: category.totalPages,
|
|
375
|
-
// Articles on this page → generates ItemList schema
|
|
376
|
-
items: category.articles.map((a, i) => ({
|
|
377
|
-
name: a.title,
|
|
378
|
-
url: `https://techblog.com/blog/${a.slug}`,
|
|
379
|
-
image: a.cover_image,
|
|
380
|
-
position: (page - 1) * 10 + i + 1,
|
|
381
|
-
})),
|
|
382
|
-
}, siteConfig);
|
|
383
|
-
|
|
384
|
-
return metadata;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
export default async function CategoryPage({ params, searchParams }) {
|
|
388
|
-
const page = Number(searchParams.page) || 1;
|
|
389
|
-
const category = await fetchCategory(params.slug);
|
|
390
|
-
const { schemas } = generateCategoryMetadata({ /* same data */ }, siteConfig);
|
|
391
|
-
|
|
392
|
-
return (
|
|
393
|
-
<>
|
|
394
|
-
<JsonLd schema={schemas} graph />
|
|
395
|
-
<h1>{category.name}</h1>
|
|
396
|
-
<ArticleGrid articles={category.articles} />
|
|
397
|
-
<Pagination current={page} total={category.totalPages} />
|
|
398
|
-
</>
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
> **Pagination SEO is automatic!** The helper generates:
|
|
404
|
-
> - `rel="canonical"` (without `?page=1` for page 1)
|
|
405
|
-
> - `rel="prev"` / `rel="next"` links
|
|
406
|
-
> - Page number in title (e.g. "JavaScript - Page 2")
|
|
407
|
-
|
|
408
|
-
### Author Page
|
|
409
|
-
|
|
410
|
-
```tsx
|
|
411
|
-
// app/authors/[slug]/page.tsx — Server Component
|
|
412
|
-
import {
|
|
413
|
-
toNextMetadata,
|
|
414
|
-
generateWebPageSchema,
|
|
415
|
-
generateOrganizationSchema,
|
|
416
|
-
JsonLd
|
|
417
|
-
} from '@masters-ws/react-seo/core';
|
|
418
|
-
|
|
419
|
-
export async function generateMetadata({ params }) {
|
|
420
|
-
const author = await fetchAuthor(params.slug);
|
|
421
|
-
|
|
422
|
-
return toNextMetadata({
|
|
423
|
-
title: `${author.name} — Author at Tech Blog`,
|
|
424
|
-
description: author.bio,
|
|
425
|
-
image: author.avatar,
|
|
426
|
-
type: 'profile',
|
|
427
|
-
canonical: `https://techblog.com/authors/${params.slug}`,
|
|
428
|
-
}, siteConfig);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export default async function AuthorPage({ params }) {
|
|
432
|
-
const author = await fetchAuthor(params.slug);
|
|
433
|
-
|
|
434
|
-
const schemas = [
|
|
435
|
-
{
|
|
436
|
-
"@context": "https://schema.org",
|
|
437
|
-
"@type": "Person",
|
|
438
|
-
"name": author.name,
|
|
439
|
-
"description": author.bio,
|
|
440
|
-
"image": author.avatar,
|
|
441
|
-
"jobTitle": author.job_title,
|
|
442
|
-
"url": `https://techblog.com/authors/${params.slug}`,
|
|
443
|
-
"sameAs": author.social_links,
|
|
444
|
-
},
|
|
445
|
-
generateOrganizationSchema(siteConfig),
|
|
446
|
-
];
|
|
447
|
-
|
|
448
|
-
return (
|
|
449
|
-
<>
|
|
450
|
-
<JsonLd schema={schemas} graph />
|
|
451
|
-
<h1>{author.name}</h1>
|
|
452
|
-
<p>{author.bio}</p>
|
|
453
|
-
<ArticleGrid articles={author.articles} />
|
|
454
|
-
</>
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### Tag Page
|
|
460
|
-
|
|
461
|
-
```tsx
|
|
462
|
-
// app/tag/[slug]/page.tsx — Server Component
|
|
463
|
-
import { generateCategoryMetadata, JsonLd } from '@masters-ws/react-seo/core';
|
|
464
|
-
|
|
465
|
-
export async function generateMetadata({ params, searchParams }) {
|
|
466
|
-
const page = Number(searchParams.page) || 1;
|
|
467
|
-
const tag = await fetchTag(params.slug);
|
|
468
|
-
|
|
469
|
-
// Reuse generateCategoryMetadata — tags are just another type of collection!
|
|
470
|
-
const { metadata } = generateCategoryMetadata({
|
|
471
|
-
name: `Tag: ${tag.name}`,
|
|
472
|
-
description: tag.description || `All articles tagged with "${tag.name}"`,
|
|
473
|
-
url: `https://techblog.com/tag/${params.slug}`,
|
|
474
|
-
page,
|
|
475
|
-
totalPages: tag.totalPages,
|
|
476
|
-
noindex: tag.totalPages <= 1, // noindex thin tag pages
|
|
477
|
-
}, siteConfig);
|
|
478
|
-
|
|
479
|
-
return metadata;
|
|
480
|
-
}
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
### Article with FAQ Section
|
|
484
|
-
|
|
485
|
-
```tsx
|
|
486
|
-
// app/blog/[slug]/page.tsx — If the article has an FAQ section
|
|
487
|
-
import { generateArticleMetadata, generateFAQSchema, JsonLd } from '@masters-ws/react-seo/core';
|
|
488
|
-
|
|
489
|
-
export default async function ArticlePage({ params }) {
|
|
490
|
-
const article = await fetchArticle(params.slug);
|
|
491
|
-
const { schemas: articleSchemas } = generateArticleMetadata({ /* ... */ }, siteConfig);
|
|
492
|
-
|
|
493
|
-
// Add FAQ schema if article has FAQ section
|
|
494
|
-
const allSchemas = [...articleSchemas];
|
|
495
|
-
if (article.faqs?.length > 0) {
|
|
496
|
-
allSchemas.push(generateFAQSchema(
|
|
497
|
-
article.faqs.map(f => ({ q: f.question, a: f.answer }))
|
|
498
|
-
));
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return (
|
|
502
|
-
<>
|
|
503
|
-
<JsonLd schema={allSchemas} graph />
|
|
504
|
-
<article>
|
|
505
|
-
<h1>{article.title}</h1>
|
|
506
|
-
<div>{article.content}</div>
|
|
507
|
-
{article.faqs?.length > 0 && (
|
|
508
|
-
<section>
|
|
509
|
-
<h2>Frequently Asked Questions</h2>
|
|
510
|
-
{article.faqs.map(faq => (
|
|
511
|
-
<details key={faq.question}>
|
|
512
|
-
<summary>{faq.question}</summary>
|
|
513
|
-
<p>{faq.answer}</p>
|
|
514
|
-
</details>
|
|
515
|
-
))}
|
|
516
|
-
</section>
|
|
517
|
-
)}
|
|
518
|
-
</article>
|
|
519
|
-
</>
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
---
|
|
525
|
-
|
|
526
|
-
## 🏢 Use Case: Showcase / Branding Website
|
|
527
|
-
|
|
528
|
-
A complete guide for corporate sites, portfolios, landing pages, and service-based businesses.
|
|
529
|
-
|
|
530
|
-
### Homepage
|
|
531
|
-
|
|
532
|
-
```tsx
|
|
533
|
-
// app/page.tsx — Server Component
|
|
534
|
-
import { generateHomepageMetadata, JsonLd } from '@masters-ws/react-seo/core';
|
|
535
|
-
|
|
536
|
-
const siteConfig = {
|
|
537
|
-
name: "Acme Solutions",
|
|
538
|
-
url: "https://acme.com",
|
|
539
|
-
logo: "https://acme.com/logo.png",
|
|
540
|
-
description: "Digital solutions for modern businesses",
|
|
541
|
-
language: "en_US",
|
|
542
|
-
twitterHandle: "@acme",
|
|
543
|
-
socialLinks: [
|
|
544
|
-
"https://twitter.com/acme",
|
|
545
|
-
"https://linkedin.com/company/acme",
|
|
546
|
-
"https://github.com/acme",
|
|
547
|
-
],
|
|
548
|
-
};
|
|
549
|
-
|
|
550
|
-
export async function generateMetadata() {
|
|
551
|
-
const { metadata } = generateHomepageMetadata({
|
|
552
|
-
title: "Acme Solutions — Digital Solutions for Modern Businesses",
|
|
553
|
-
description: "We help businesses grow with cutting-edge web development, mobile apps, and cloud solutions.",
|
|
554
|
-
ogImage: "https://acme.com/og-image.jpg",
|
|
555
|
-
|
|
556
|
-
// Add LocalBusiness if you have a physical office
|
|
557
|
-
localBusiness: {
|
|
558
|
-
name: "Acme Solutions HQ",
|
|
559
|
-
description: "Software development company",
|
|
560
|
-
telephone: "+1-555-123-4567",
|
|
561
|
-
address: {
|
|
562
|
-
street: "123 Tech Avenue",
|
|
563
|
-
city: "San Francisco",
|
|
564
|
-
region: "CA",
|
|
565
|
-
postalCode: "94105",
|
|
566
|
-
country: "US",
|
|
567
|
-
},
|
|
568
|
-
geo: { lat: 37.7749, lng: -122.4194 },
|
|
569
|
-
openingHours: ["Mo-Fr 09:00-18:00"],
|
|
570
|
-
},
|
|
571
|
-
}, siteConfig);
|
|
572
|
-
|
|
573
|
-
return metadata;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
export default function HomePage() {
|
|
577
|
-
const { schemas } = generateHomepageMetadata({ /* same data */ }, siteConfig);
|
|
578
|
-
|
|
579
|
-
return (
|
|
580
|
-
<>
|
|
581
|
-
<JsonLd schema={schemas} graph />
|
|
582
|
-
<main>
|
|
583
|
-
<section id="hero">
|
|
584
|
-
<h1>Build. Scale. Succeed.</h1>
|
|
585
|
-
<p>Digital solutions for modern businesses</p>
|
|
586
|
-
</section>
|
|
587
|
-
<section id="services">...</section>
|
|
588
|
-
<section id="portfolio">...</section>
|
|
589
|
-
<section id="contact">...</section>
|
|
590
|
-
</main>
|
|
591
|
-
</>
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
```
|
|
595
|
-
|
|
596
|
-
**What Google sees for a local business:**
|
|
597
|
-
```json
|
|
598
|
-
{
|
|
599
|
-
"@context": "https://schema.org",
|
|
600
|
-
"@graph": [
|
|
601
|
-
{ "@type": "WebPage", "name": "Acme Solutions", "url": "https://acme.com" },
|
|
602
|
-
{ "@type": "Organization", "name": "Acme Solutions", "logo": "...", "sameAs": [...] },
|
|
603
|
-
{ "@type": "WebSite", "potentialAction": { "@type": "SearchAction", ... } },
|
|
604
|
-
{
|
|
605
|
-
"@type": "LocalBusiness",
|
|
606
|
-
"name": "Acme Solutions HQ",
|
|
607
|
-
"telephone": "+1-555-123-4567",
|
|
608
|
-
"address": { "@type": "PostalAddress", "streetAddress": "123 Tech Avenue", ... },
|
|
609
|
-
"geo": { "@type": "GeoCoordinates", "latitude": 37.7749, "longitude": -122.4194 },
|
|
610
|
-
"openingHours": ["Mo-Fr 09:00-18:00"]
|
|
611
|
-
}
|
|
612
|
-
]
|
|
613
|
-
}
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
### About Page
|
|
617
|
-
|
|
618
|
-
```tsx
|
|
619
|
-
// app/about/page.tsx — Server Component
|
|
620
|
-
import {
|
|
621
|
-
toNextMetadata,
|
|
622
|
-
generateWebPageSchema,
|
|
623
|
-
generateOrganizationSchema,
|
|
624
|
-
generateBreadcrumbSchema,
|
|
625
|
-
JsonLd
|
|
626
|
-
} from '@masters-ws/react-seo/core';
|
|
627
|
-
|
|
628
|
-
export async function generateMetadata() {
|
|
629
|
-
return toNextMetadata({
|
|
630
|
-
title: "About Us — Our Story & Mission",
|
|
631
|
-
description: "Learn about Acme Solutions, our mission, team, and the values that drive us to build amazing digital products.",
|
|
632
|
-
image: "https://acme.com/about-og.jpg",
|
|
633
|
-
type: 'website',
|
|
634
|
-
canonical: "https://acme.com/about",
|
|
635
|
-
}, siteConfig);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
export default function AboutPage() {
|
|
639
|
-
const schemas = [
|
|
640
|
-
generateWebPageSchema({
|
|
641
|
-
name: "About Acme Solutions",
|
|
642
|
-
description: "Our story, mission, and team",
|
|
643
|
-
url: "https://acme.com/about",
|
|
644
|
-
image: "https://acme.com/team-photo.jpg",
|
|
645
|
-
}, siteConfig),
|
|
646
|
-
generateBreadcrumbSchema([
|
|
647
|
-
{ name: "Home", item: "https://acme.com" },
|
|
648
|
-
{ name: "About Us", item: "https://acme.com/about" },
|
|
649
|
-
]),
|
|
650
|
-
generateOrganizationSchema(siteConfig),
|
|
651
|
-
];
|
|
652
|
-
|
|
653
|
-
return (
|
|
654
|
-
<>
|
|
655
|
-
<JsonLd schema={schemas} graph />
|
|
656
|
-
<main>
|
|
657
|
-
<h1>About Us</h1>
|
|
658
|
-
<p>Founded in 2020, Acme Solutions...</p>
|
|
659
|
-
</main>
|
|
660
|
-
</>
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
### Services Page
|
|
666
|
-
|
|
667
|
-
```tsx
|
|
668
|
-
// app/services/page.tsx — Server Component
|
|
669
|
-
import {
|
|
670
|
-
toNextMetadata,
|
|
671
|
-
generateWebPageSchema,
|
|
672
|
-
generateBreadcrumbSchema,
|
|
673
|
-
generateFAQSchema,
|
|
674
|
-
JsonLd
|
|
675
|
-
} from '@masters-ws/react-seo/core';
|
|
676
|
-
|
|
677
|
-
export async function generateMetadata() {
|
|
678
|
-
return toNextMetadata({
|
|
679
|
-
title: "Our Services — Web Development, Mobile Apps & Cloud",
|
|
680
|
-
description: "Explore our professional services: custom web development, mobile app design, cloud infrastructure, and digital consulting.",
|
|
681
|
-
image: "https://acme.com/services-og.jpg",
|
|
682
|
-
type: 'website',
|
|
683
|
-
canonical: "https://acme.com/services",
|
|
684
|
-
}, siteConfig);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
export default function ServicesPage() {
|
|
688
|
-
const schemas = [
|
|
689
|
-
generateWebPageSchema({
|
|
690
|
-
name: "Our Services",
|
|
691
|
-
description: "Professional digital services",
|
|
692
|
-
url: "https://acme.com/services",
|
|
693
|
-
}, siteConfig),
|
|
694
|
-
generateBreadcrumbSchema([
|
|
695
|
-
{ name: "Home", item: "https://acme.com" },
|
|
696
|
-
{ name: "Services", item: "https://acme.com/services" },
|
|
697
|
-
]),
|
|
698
|
-
// Add FAQ for common service questions → shows in Google rich results!
|
|
699
|
-
generateFAQSchema([
|
|
700
|
-
{ q: "What technologies do you use?", a: "We specialize in React, Next.js, Node.js, and cloud platforms like AWS and GCP." },
|
|
701
|
-
{ q: "How long does a typical project take?", a: "Most projects take 4-12 weeks depending on complexity." },
|
|
702
|
-
{ q: "Do you offer ongoing maintenance?", a: "Yes, we offer monthly maintenance plans starting at $500/month." },
|
|
703
|
-
]),
|
|
704
|
-
];
|
|
705
|
-
|
|
706
|
-
return (
|
|
707
|
-
<>
|
|
708
|
-
<JsonLd schema={schemas} graph />
|
|
709
|
-
<main>
|
|
710
|
-
<h1>Our Services</h1>
|
|
711
|
-
<ServiceCard title="Web Development" description="..." />
|
|
712
|
-
<ServiceCard title="Mobile Apps" description="..." />
|
|
713
|
-
<ServiceCard title="Cloud Solutions" description="..." />
|
|
714
|
-
|
|
715
|
-
<section>
|
|
716
|
-
<h2>Frequently Asked Questions</h2>
|
|
717
|
-
{/* FAQ content */}
|
|
718
|
-
</section>
|
|
719
|
-
</main>
|
|
720
|
-
</>
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
### Individual Service Page
|
|
726
|
-
|
|
727
|
-
```tsx
|
|
728
|
-
// app/services/[slug]/page.tsx — Server Component
|
|
729
|
-
import {
|
|
730
|
-
toNextMetadata,
|
|
731
|
-
generateWebPageSchema,
|
|
732
|
-
generateBreadcrumbSchema,
|
|
733
|
-
generateHowToSchema,
|
|
734
|
-
generateFAQSchema,
|
|
735
|
-
JsonLd
|
|
736
|
-
} from '@masters-ws/react-seo/core';
|
|
737
|
-
|
|
738
|
-
export async function generateMetadata({ params }) {
|
|
739
|
-
const service = await fetchService(params.slug);
|
|
740
|
-
|
|
741
|
-
return toNextMetadata({
|
|
742
|
-
title: `${service.name} — Professional ${service.name} Services`,
|
|
743
|
-
description: service.meta_description,
|
|
744
|
-
image: service.og_image,
|
|
745
|
-
type: 'website',
|
|
746
|
-
canonical: `https://acme.com/services/${params.slug}`,
|
|
747
|
-
}, siteConfig);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
export default async function ServicePage({ params }) {
|
|
751
|
-
const service = await fetchService(params.slug);
|
|
752
|
-
|
|
753
|
-
const schemas = [
|
|
754
|
-
generateWebPageSchema({
|
|
755
|
-
name: service.name,
|
|
756
|
-
description: service.description,
|
|
757
|
-
url: `https://acme.com/services/${params.slug}`,
|
|
758
|
-
}, siteConfig),
|
|
759
|
-
generateBreadcrumbSchema([
|
|
760
|
-
{ name: "Home", item: "https://acme.com" },
|
|
761
|
-
{ name: "Services", item: "https://acme.com/services" },
|
|
762
|
-
{ name: service.name, item: `https://acme.com/services/${params.slug}` },
|
|
763
|
-
]),
|
|
764
|
-
];
|
|
765
|
-
|
|
766
|
-
// Add HowTo schema if the service has a process
|
|
767
|
-
if (service.process_steps) {
|
|
768
|
-
schemas.push(generateHowToSchema({
|
|
769
|
-
name: `How We Deliver ${service.name}`,
|
|
770
|
-
description: `Our step-by-step process for ${service.name}`,
|
|
771
|
-
steps: service.process_steps.map(s => ({
|
|
772
|
-
name: s.title,
|
|
773
|
-
text: s.description,
|
|
774
|
-
})),
|
|
775
|
-
}));
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Add FAQ if available
|
|
779
|
-
if (service.faqs?.length > 0) {
|
|
780
|
-
schemas.push(generateFAQSchema(
|
|
781
|
-
service.faqs.map(f => ({ q: f.question, a: f.answer }))
|
|
782
|
-
));
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
return (
|
|
786
|
-
<>
|
|
787
|
-
<JsonLd schema={schemas} graph />
|
|
788
|
-
<main>
|
|
789
|
-
<h1>{service.name}</h1>
|
|
790
|
-
<div>{service.content}</div>
|
|
791
|
-
</main>
|
|
792
|
-
</>
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
```
|
|
796
|
-
|
|
797
|
-
### Contact Page
|
|
798
|
-
|
|
799
|
-
```tsx
|
|
800
|
-
// app/contact/page.tsx — Server Component
|
|
801
|
-
import {
|
|
802
|
-
toNextMetadata,
|
|
803
|
-
generateWebPageSchema,
|
|
804
|
-
generateBreadcrumbSchema,
|
|
805
|
-
generateLocalBusinessSchema,
|
|
806
|
-
JsonLd
|
|
807
|
-
} from '@masters-ws/react-seo/core';
|
|
808
|
-
|
|
809
|
-
export async function generateMetadata() {
|
|
810
|
-
return toNextMetadata({
|
|
811
|
-
title: "Contact Us — Get In Touch",
|
|
812
|
-
description: "Have a project in mind? Contact Acme Solutions for a free consultation. We respond within 24 hours.",
|
|
813
|
-
type: 'website',
|
|
814
|
-
canonical: "https://acme.com/contact",
|
|
815
|
-
}, siteConfig);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
export default function ContactPage() {
|
|
819
|
-
const schemas = [
|
|
820
|
-
generateWebPageSchema({
|
|
821
|
-
name: "Contact Us",
|
|
822
|
-
description: "Get in touch with our team",
|
|
823
|
-
url: "https://acme.com/contact",
|
|
824
|
-
}, siteConfig),
|
|
825
|
-
generateBreadcrumbSchema([
|
|
826
|
-
{ name: "Home", item: "https://acme.com" },
|
|
827
|
-
{ name: "Contact", item: "https://acme.com/contact" },
|
|
828
|
-
]),
|
|
829
|
-
generateLocalBusinessSchema({
|
|
830
|
-
name: "Acme Solutions",
|
|
831
|
-
description: "Digital solutions company",
|
|
832
|
-
telephone: "+1-555-123-4567",
|
|
833
|
-
address: {
|
|
834
|
-
street: "123 Tech Avenue",
|
|
835
|
-
city: "San Francisco",
|
|
836
|
-
region: "CA",
|
|
837
|
-
postalCode: "94105",
|
|
838
|
-
country: "US",
|
|
839
|
-
},
|
|
840
|
-
geo: { lat: 37.7749, lng: -122.4194 },
|
|
841
|
-
openingHours: ["Mo-Fr 09:00-18:00"],
|
|
842
|
-
}),
|
|
843
|
-
];
|
|
844
|
-
|
|
845
|
-
return (
|
|
846
|
-
<>
|
|
847
|
-
<JsonLd schema={schemas} graph />
|
|
848
|
-
<main>
|
|
849
|
-
<h1>Contact Us</h1>
|
|
850
|
-
<ContactForm />
|
|
851
|
-
<Map />
|
|
852
|
-
</main>
|
|
853
|
-
</>
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
### Portfolio / Case Study Page
|
|
859
|
-
|
|
860
|
-
```tsx
|
|
861
|
-
// app/portfolio/[slug]/page.tsx
|
|
862
|
-
import {
|
|
863
|
-
toNextMetadata,
|
|
864
|
-
generateWebPageSchema,
|
|
865
|
-
generateBreadcrumbSchema,
|
|
866
|
-
JsonLd,
|
|
867
|
-
cleanSchema,
|
|
868
|
-
} from '@masters-ws/react-seo/core';
|
|
869
|
-
|
|
870
|
-
export async function generateMetadata({ params }) {
|
|
871
|
-
const project = await fetchProject(params.slug);
|
|
872
|
-
|
|
873
|
-
return toNextMetadata({
|
|
874
|
-
title: `${project.name} — Case Study`,
|
|
875
|
-
description: project.summary,
|
|
876
|
-
image: project.cover_image,
|
|
877
|
-
type: 'website',
|
|
878
|
-
canonical: `https://acme.com/portfolio/${params.slug}`,
|
|
879
|
-
}, siteConfig);
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
export default async function CaseStudyPage({ params }) {
|
|
883
|
-
const project = await fetchProject(params.slug);
|
|
884
|
-
|
|
885
|
-
const schemas = [
|
|
886
|
-
generateWebPageSchema({
|
|
887
|
-
name: project.name,
|
|
888
|
-
description: project.summary,
|
|
889
|
-
url: `https://acme.com/portfolio/${params.slug}`,
|
|
890
|
-
image: project.cover_image,
|
|
891
|
-
datePublished: project.completed_at,
|
|
892
|
-
}, siteConfig),
|
|
893
|
-
generateBreadcrumbSchema([
|
|
894
|
-
{ name: "Home", item: "https://acme.com" },
|
|
895
|
-
{ name: "Portfolio", item: "https://acme.com/portfolio" },
|
|
896
|
-
{ name: project.name, item: `https://acme.com/portfolio/${params.slug}` },
|
|
897
|
-
]),
|
|
898
|
-
// Custom CreativeWork schema for portfolio items
|
|
899
|
-
cleanSchema({
|
|
900
|
-
"@context": "https://schema.org",
|
|
901
|
-
"@type": "CreativeWork",
|
|
902
|
-
"name": project.name,
|
|
903
|
-
"description": project.summary,
|
|
904
|
-
"image": project.cover_image,
|
|
905
|
-
"dateCreated": project.completed_at,
|
|
906
|
-
"creator": {
|
|
907
|
-
"@type": "Organization",
|
|
908
|
-
"name": siteConfig.name,
|
|
909
|
-
},
|
|
910
|
-
"url": `https://acme.com/portfolio/${params.slug}`,
|
|
911
|
-
}),
|
|
912
|
-
];
|
|
913
|
-
|
|
914
|
-
return (
|
|
915
|
-
<>
|
|
916
|
-
<JsonLd schema={schemas} graph />
|
|
917
|
-
<main>
|
|
918
|
-
<h1>{project.name}</h1>
|
|
919
|
-
<p>{project.summary}</p>
|
|
920
|
-
</main>
|
|
921
|
-
</>
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
### Careers / Job Listing Page
|
|
927
|
-
|
|
928
|
-
```tsx
|
|
929
|
-
// app/careers/[slug]/page.tsx
|
|
930
|
-
import {
|
|
931
|
-
toNextMetadata,
|
|
932
|
-
generateJobPostingSchema,
|
|
933
|
-
generateBreadcrumbSchema,
|
|
934
|
-
JsonLd
|
|
935
|
-
} from '@masters-ws/react-seo/core';
|
|
936
|
-
|
|
937
|
-
export async function generateMetadata({ params }) {
|
|
938
|
-
const job = await fetchJob(params.slug);
|
|
939
|
-
|
|
940
|
-
return toNextMetadata({
|
|
941
|
-
title: `${job.title} — Join Our Team`,
|
|
942
|
-
description: `We're hiring a ${job.title}. ${job.summary}`,
|
|
943
|
-
type: 'website',
|
|
944
|
-
canonical: `https://acme.com/careers/${params.slug}`,
|
|
945
|
-
}, siteConfig);
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
export default async function JobPage({ params }) {
|
|
949
|
-
const job = await fetchJob(params.slug);
|
|
950
|
-
|
|
951
|
-
const schemas = [
|
|
952
|
-
generateJobPostingSchema({
|
|
953
|
-
title: job.title,
|
|
954
|
-
description: job.full_description,
|
|
955
|
-
datePosted: job.posted_at,
|
|
956
|
-
validThrough: job.expires_at,
|
|
957
|
-
employmentType: job.type, // "FULL_TIME", "CONTRACTOR", etc.
|
|
958
|
-
remote: job.is_remote,
|
|
959
|
-
hiringOrganization: {
|
|
960
|
-
name: siteConfig.name,
|
|
961
|
-
sameAs: siteConfig.url,
|
|
962
|
-
logo: siteConfig.logo,
|
|
963
|
-
},
|
|
964
|
-
jobLocation: {
|
|
965
|
-
streetAddress: "123 Tech Avenue",
|
|
966
|
-
addressLocality: "San Francisco",
|
|
967
|
-
addressRegion: "CA",
|
|
968
|
-
addressCountry: "US",
|
|
969
|
-
},
|
|
970
|
-
baseSalary: job.salary ? {
|
|
971
|
-
currency: "USD",
|
|
972
|
-
value: { minValue: job.salary.min, maxValue: job.salary.max },
|
|
973
|
-
unitText: "YEAR",
|
|
974
|
-
} : undefined,
|
|
975
|
-
}),
|
|
976
|
-
generateBreadcrumbSchema([
|
|
977
|
-
{ name: "Home", item: "https://acme.com" },
|
|
978
|
-
{ name: "Careers", item: "https://acme.com/careers" },
|
|
979
|
-
{ name: job.title, item: `https://acme.com/careers/${params.slug}` },
|
|
980
|
-
]),
|
|
981
|
-
];
|
|
982
|
-
|
|
983
|
-
return (
|
|
984
|
-
<>
|
|
985
|
-
<JsonLd schema={schemas} graph />
|
|
986
|
-
<main>
|
|
987
|
-
<h1>{job.title}</h1>
|
|
988
|
-
<JobDetails job={job} />
|
|
989
|
-
<ApplyButton />
|
|
990
|
-
</main>
|
|
991
|
-
</>
|
|
992
|
-
);
|
|
993
|
-
}
|
|
994
|
-
```
|
|
995
|
-
|
|
996
|
-
---
|
|
997
|
-
|
|
998
|
-
## 🛒 Use Case: E-Commerce Store
|
|
999
|
-
|
|
1000
|
-
See the [Quick Start](#-quick-start-nextjs-app-router-recommended) section above for complete examples:
|
|
1001
|
-
- **[Product Page](#product-page-one-call-setup)** — with reviews, return policy, shipping, variants
|
|
1002
|
-
- **[Category Page](#category-page-with-pagination)** — with pagination and ItemList schema
|
|
1003
|
-
|
|
1004
|
-
---
|
|
1005
|
-
|
|
1006
|
-
## Manual Approach (More Control)
|
|
1007
|
-
|
|
1008
|
-
Use individual functions for granular control:
|
|
1009
|
-
|
|
1010
|
-
```tsx
|
|
1011
|
-
import {
|
|
1012
|
-
toNextMetadata,
|
|
1013
|
-
generateProductSchema,
|
|
1014
|
-
generateBreadcrumbSchema,
|
|
1015
|
-
generateOrganizationSchema,
|
|
1016
|
-
generateWebPageSchema,
|
|
1017
|
-
JsonLd
|
|
1018
|
-
} from '@masters-ws/react-seo/core';
|
|
1019
|
-
|
|
1020
|
-
export async function generateMetadata({ params }) {
|
|
1021
|
-
const product = await fetchProduct(params.slug);
|
|
1022
|
-
|
|
1023
|
-
return toNextMetadata({
|
|
1024
|
-
title: product.meta_title || product.name,
|
|
1025
|
-
description: product.meta_description,
|
|
1026
|
-
image: product.main_image?.url,
|
|
1027
|
-
type: 'product',
|
|
1028
|
-
canonical: `https://store.com/products/${params.slug}`,
|
|
1029
|
-
product: {
|
|
1030
|
-
sku: product.sku,
|
|
1031
|
-
brand: product.brand?.name,
|
|
1032
|
-
price: product.price,
|
|
1033
|
-
currency: "USD",
|
|
1034
|
-
availability: "https://schema.org/InStock",
|
|
1035
|
-
}
|
|
1036
|
-
}, siteConfig);
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
export default async function ProductPage({ params }) {
|
|
1040
|
-
const product = await fetchProduct(params.slug);
|
|
1041
|
-
|
|
1042
|
-
const schemas = [
|
|
1043
|
-
generateProductSchema({
|
|
1044
|
-
name: product.name,
|
|
1045
|
-
description: product.description,
|
|
1046
|
-
image: [product.main_image, ...product.gallery],
|
|
1047
|
-
price: product.price,
|
|
1048
|
-
currency: "USD",
|
|
1049
|
-
url: `https://store.com/products/${params.slug}`,
|
|
1050
|
-
reviews: [
|
|
1051
|
-
{ author: "John", ratingValue: 5, reviewBody: "Excellent product!" },
|
|
1052
|
-
],
|
|
1053
|
-
returnPolicy: {
|
|
1054
|
-
returnPolicyCategory: 'MerchantReturnFiniteReturnWindow',
|
|
1055
|
-
returnWithin: 14,
|
|
1056
|
-
},
|
|
1057
|
-
shipping: {
|
|
1058
|
-
shippingRate: { value: 0, currency: "USD" },
|
|
1059
|
-
deliveryTime: { minDays: 2, maxDays: 5 },
|
|
1060
|
-
},
|
|
1061
|
-
}),
|
|
1062
|
-
generateBreadcrumbSchema([
|
|
1063
|
-
{ name: "Home", item: "https://store.com" },
|
|
1064
|
-
{ name: product.name, item: `https://store.com/products/${params.slug}` },
|
|
1065
|
-
]),
|
|
1066
|
-
];
|
|
1067
|
-
|
|
1068
|
-
return (
|
|
1069
|
-
<>
|
|
1070
|
-
<JsonLd schema={schemas} graph />
|
|
1071
|
-
<ProductDetailClient product={product} />
|
|
1072
|
-
</>
|
|
1073
|
-
);
|
|
1074
|
-
}
|
|
1075
|
-
```
|
|
1076
|
-
|
|
1077
|
-
---
|
|
1078
|
-
|
|
1079
|
-
## `<JsonLd>` Component
|
|
1080
|
-
|
|
1081
|
-
Server-safe JSON-LD renderer. Works in Server Components (no `'use client'` needed).
|
|
1082
|
-
|
|
1083
|
-
### Props
|
|
1084
|
-
|
|
1085
|
-
| Prop | Type | Default | Description |
|
|
1086
|
-
| :--- | :--- | :--- | :--- |
|
|
1087
|
-
| `schema` | `object \| object[]` | — | Schema object(s) to render |
|
|
1088
|
-
| `graph` | `boolean` | `false` | Combine schemas into a single `@graph` block |
|
|
1089
|
-
|
|
1090
|
-
### Separate Scripts (default)
|
|
1091
|
-
```tsx
|
|
1092
|
-
<JsonLd schema={[productSchema, breadcrumbSchema]} />
|
|
1093
|
-
// Renders 2 separate <script type="application/ld+json"> tags
|
|
1094
|
-
```
|
|
1095
|
-
|
|
1096
|
-
### @graph Pattern (recommended for multiple schemas)
|
|
1097
|
-
```tsx
|
|
1098
|
-
<JsonLd schema={[productSchema, breadcrumbSchema, orgSchema]} graph />
|
|
1099
|
-
// Renders 1 <script> with @context + @graph containing all schemas
|
|
1100
|
-
```
|
|
1101
|
-
|
|
1102
|
-
---
|
|
1103
|
-
|
|
1104
|
-
## Usage: React / Pages Router (With Components)
|
|
1105
|
-
|
|
1106
|
-
```tsx
|
|
1107
|
-
// _app.tsx
|
|
1108
|
-
import { SEOProvider } from '@masters-ws/react-seo';
|
|
1109
|
-
|
|
1110
|
-
const config = { name: "My Site", url: "https://mysite.com", description: "Awesome site" };
|
|
1111
|
-
|
|
1112
|
-
export default function App({ Component, pageProps }) {
|
|
1113
|
-
return (
|
|
1114
|
-
<SEOProvider config={config}>
|
|
1115
|
-
<Component {...pageProps} />
|
|
1116
|
-
</SEOProvider>
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
```
|
|
1120
|
-
|
|
1121
|
-
```tsx
|
|
1122
|
-
// pages/products/[id].tsx
|
|
1123
|
-
import { SeoProduct } from '@masters-ws/react-seo';
|
|
1124
|
-
|
|
1125
|
-
export default function ProductPage({ product }) {
|
|
1126
|
-
return (
|
|
1127
|
-
<>
|
|
1128
|
-
<SeoProduct item={{
|
|
1129
|
-
name: product.name,
|
|
1130
|
-
description: product.description,
|
|
1131
|
-
image: product.image,
|
|
1132
|
-
price: product.price,
|
|
1133
|
-
currency: "USD",
|
|
1134
|
-
sku: product.sku,
|
|
1135
|
-
brand: product.brand,
|
|
1136
|
-
}} />
|
|
1137
|
-
<main>...</main>
|
|
1138
|
-
</>
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1141
|
-
```
|
|
1142
|
-
|
|
1143
|
-
---
|
|
1144
|
-
|
|
1145
|
-
## Core Functions Reference
|
|
1146
|
-
|
|
1147
|
-
### Convenience Helpers (Recommended — One-Call Setup)
|
|
1148
|
-
|
|
1149
|
-
| Function | Description |
|
|
1150
|
-
| :--- | :--- |
|
|
1151
|
-
| `generateProductMetadata(product, config)` | Generates Metadata + Product, Breadcrumb, Organization, WebSite schemas |
|
|
1152
|
-
| `generateArticleMetadata(article, config)` | Generates Metadata + Article, Breadcrumb, Organization, WebSite schemas |
|
|
1153
|
-
| `generateCategoryMetadata(category, config)` | Generates Metadata + CollectionPage, Breadcrumb, Organization, ItemList schemas |
|
|
1154
|
-
| `generateHomepageMetadata(input, config)` | Generates Metadata + WebPage, Organization, WebSite, optional LocalBusiness schemas |
|
|
1155
|
-
|
|
1156
|
-
### Metadata Functions
|
|
1157
|
-
|
|
1158
|
-
| Function | Description |
|
|
1159
|
-
| :--- | :--- |
|
|
1160
|
-
| `toNextMetadata(data, config)` | Converts SEO data to Next.js Metadata object (OG, Twitter, Canonical, etc.) |
|
|
1161
|
-
| `generatePaginationLinks(url, page, total)` | Returns `prev` / `next` / `canonical` URLs |
|
|
1162
|
-
| `generatePaginatedTitle(title, page, suffix)` | Appends page number to title |
|
|
1163
|
-
|
|
1164
|
-
### Schema Generator Functions
|
|
1165
|
-
|
|
1166
|
-
| Function | Description |
|
|
1167
|
-
| :--- | :--- |
|
|
1168
|
-
| `generateProductSchema(data)` | `Product` with multi-image, reviews, return policy, shipping, variants |
|
|
1169
|
-
| `generateArticleSchema(data, config)` | `NewsArticle` with author and publisher |
|
|
1170
|
-
| `generateFAQSchema(questions)` | `FAQPage` |
|
|
1171
|
-
| `generateBreadcrumbSchema(items)` | `BreadcrumbList` |
|
|
1172
|
-
| `generateVideoSchema(data)` | `VideoObject` |
|
|
1173
|
-
| `generateEventSchema(data)` | `Event` (online & offline) |
|
|
1174
|
-
| `generateLocalBusinessSchema(data)` | `LocalBusiness` with geo and opening hours |
|
|
1175
|
-
| `generateOrganizationSchema(config)` | `Organization` |
|
|
1176
|
-
| `generateWebSiteSchema(config)` | `WebSite` with `SearchAction` |
|
|
1177
|
-
| `generateWebPageSchema(data, config)` | `WebPage` |
|
|
1178
|
-
| `generateCollectionPageSchema(data, config)` | `CollectionPage` (for categories/archives) |
|
|
1179
|
-
| `generateItemListSchema(data)` | `ItemList` (for product listings) |
|
|
1180
|
-
| `generateHowToSchema(data)` | `HowTo` with steps, tools, supplies |
|
|
1181
|
-
| `generateRecipeSchema(data)` | `Recipe` with ingredients and instructions |
|
|
1182
|
-
| `generateJobPostingSchema(data)` | `JobPosting` with salary and remote support |
|
|
1183
|
-
| `generateSoftwareSchema(data)` | `SoftwareApplication` |
|
|
1184
|
-
| `generateBookSchema(data)` | `Book` |
|
|
1185
|
-
| `generateMovieSchema(data)` | `Movie` |
|
|
1186
|
-
| `generatePodcastSchema(data)` | `PodcastSeries` |
|
|
1187
|
-
| `generatePodcastEpisodeSchema(data)` | `PodcastEpisode` |
|
|
1188
|
-
|
|
1189
|
-
### Utility Functions
|
|
1190
|
-
|
|
1191
|
-
| Function | Description |
|
|
1192
|
-
| :--- | :--- |
|
|
1193
|
-
| `cleanSchema(obj)` | Deep-removes `undefined`/`null` values from schema objects |
|
|
1194
|
-
| `validateSEO(type, data, fields)` | Logs dev warnings for missing required fields |
|
|
1195
|
-
|
|
1196
|
-
### Components
|
|
1197
|
-
|
|
1198
|
-
| Component | Description |
|
|
1199
|
-
| :--- | :--- |
|
|
1200
|
-
| `<JsonLd schema={..} graph? />` | Server-safe JSON-LD renderer with optional `@graph` support |
|
|
1201
|
-
|
|
1202
|
-
---
|
|
1203
|
-
|
|
1204
|
-
## Product Schema Features
|
|
1205
|
-
|
|
1206
|
-
### Multi-Image Support
|
|
1207
|
-
```tsx
|
|
1208
|
-
generateProductSchema({
|
|
1209
|
-
name: "T-Shirt",
|
|
1210
|
-
image: [ // Array of images
|
|
1211
|
-
"https://store.com/front.jpg",
|
|
1212
|
-
"https://store.com/back.jpg",
|
|
1213
|
-
"https://store.com/detail.jpg",
|
|
1214
|
-
],
|
|
1215
|
-
// ...
|
|
1216
|
-
});
|
|
1217
|
-
```
|
|
1218
|
-
|
|
1219
|
-
### Individual Reviews
|
|
1220
|
-
```tsx
|
|
1221
|
-
generateProductSchema({
|
|
1222
|
-
// ...
|
|
1223
|
-
rating: 4.5,
|
|
1224
|
-
reviewCount: 128,
|
|
1225
|
-
reviews: [
|
|
1226
|
-
{ author: "Alice", ratingValue: 5, reviewBody: "Amazing quality!", datePublished: "2024-01-15" },
|
|
1227
|
-
{ author: "Bob", ratingValue: 4, reviewBody: "Good value", datePublished: "2024-02-01" },
|
|
1228
|
-
],
|
|
1229
|
-
});
|
|
1230
|
-
```
|
|
1231
|
-
|
|
1232
|
-
### Return Policy
|
|
1233
|
-
```tsx
|
|
1234
|
-
generateProductSchema({
|
|
1235
|
-
// ...
|
|
1236
|
-
returnPolicy: {
|
|
1237
|
-
returnPolicyCategory: 'MerchantReturnFiniteReturnWindow',
|
|
1238
|
-
returnWithin: 30, // 30 days
|
|
1239
|
-
returnMethod: 'ReturnByMail',
|
|
1240
|
-
returnFees: 'FreeReturn',
|
|
1241
|
-
},
|
|
1242
|
-
});
|
|
1243
|
-
```
|
|
1244
|
-
|
|
1245
|
-
### Shipping Details
|
|
1246
|
-
```tsx
|
|
1247
|
-
generateProductSchema({
|
|
1248
|
-
// ...
|
|
1249
|
-
shipping: {
|
|
1250
|
-
shippingRate: { value: 5.99, currency: "USD" },
|
|
1251
|
-
shippingDestination: "US",
|
|
1252
|
-
deliveryTime: { minDays: 3, maxDays: 7 },
|
|
1253
|
-
freeShippingThreshold: 50, // Free shipping over $50
|
|
1254
|
-
},
|
|
1255
|
-
});
|
|
1256
|
-
```
|
|
1257
|
-
|
|
1258
|
-
### Product Variants (AggregateOffer)
|
|
1259
|
-
```tsx
|
|
1260
|
-
generateProductSchema({
|
|
1261
|
-
name: "T-Shirt",
|
|
1262
|
-
description: "...",
|
|
1263
|
-
// When variants are provided, generates AggregateOffer with lowPrice/highPrice
|
|
1264
|
-
variants: [
|
|
1265
|
-
{ name: "Small", sku: "TS-S", price: 19.99 },
|
|
1266
|
-
{ name: "Medium", sku: "TS-M", price: 22.99 },
|
|
1267
|
-
{ name: "Large", sku: "TS-L", price: 24.99 },
|
|
1268
|
-
],
|
|
1269
|
-
});
|
|
1270
|
-
// Result: AggregateOffer { lowPrice: 19.99, highPrice: 24.99, offerCount: 3 }
|
|
1271
|
-
```
|
|
1272
|
-
|
|
1273
|
-
---
|
|
1274
|
-
|
|
1275
|
-
## Development Warnings
|
|
1276
|
-
|
|
1277
|
-
In development mode (`NODE_ENV !== 'production'`), the library logs warnings for missing required fields:
|
|
1278
|
-
|
|
1279
|
-
```
|
|
1280
|
-
[react-seo] Warning: "image" is missing in Product schema. Google may not show rich results.
|
|
1281
|
-
[react-seo] Warning: "price" is missing in Product schema. Google may not show rich results.
|
|
1282
|
-
```
|
|
1283
|
-
|
|
1284
|
-
---
|
|
1285
|
-
|
|
1286
|
-
## ⚠️ Important: SSR vs CSR
|
|
1287
|
-
|
|
1288
|
-
### ✅ Correct — Server-Side (Google sees everything on first crawl)
|
|
1289
|
-
```tsx
|
|
1290
|
-
// page.tsx — NO 'use client'!
|
|
1291
|
-
import { generateProductMetadata, JsonLd } from '@masters-ws/react-seo/core';
|
|
1292
|
-
|
|
1293
|
-
export async function generateMetadata() { /* ... */ }
|
|
1294
|
-
|
|
1295
|
-
export default function Page() {
|
|
1296
|
-
return <JsonLd schema={schemas} graph />;
|
|
1297
|
-
}
|
|
1298
|
-
```
|
|
1299
|
-
|
|
1300
|
-
### ❌ Incorrect — Client-Side (Google may not see metadata)
|
|
1301
|
-
```tsx
|
|
1302
|
-
// page.tsx
|
|
1303
|
-
'use client' // ⛔ Metadata is only injected after JavaScript loads!
|
|
1304
|
-
|
|
1305
|
-
export default function Page() {
|
|
1306
|
-
return <SEO title="..." />; // Uses react-helmet → client-side only
|
|
1307
|
-
}
|
|
1308
|
-
```
|
|
1309
|
-
|
|
1310
|
-
> **Rule of thumb:** For Next.js App Router, always use `@masters-ws/react-seo/core` functions
|
|
1311
|
-
> in Server Components. Use `'use client'` only for interactive UI components,
|
|
1312
|
-
> never for SEO logic.
|
|
1313
|
-
|
|
1314
|
-
---
|
|
1315
|
-
|
|
1316
|
-
## Helmet Components Reference (Requires `react-helmet-async`)
|
|
1317
|
-
|
|
1318
|
-
These components are designed for React SPA or Next.js Pages Router:
|
|
1319
|
-
|
|
1320
|
-
| Component | Description |
|
|
1321
|
-
| :--- | :--- |
|
|
1322
|
-
| `<SEOProvider>` | Context provider — wraps your app with site config |
|
|
1323
|
-
| `<SEO />` | Main component for meta tags and inline schemas |
|
|
1324
|
-
| `<SeoArticle />` | Article/blog post SEO |
|
|
1325
|
-
| `<SeoProduct />` | E-commerce product SEO |
|
|
1326
|
-
| `<SeoFAQ />` | FAQ pages |
|
|
1327
|
-
| `<SeoVideo />` | Video content |
|
|
1328
|
-
| `<SeoEvent />` | Events and conferences |
|
|
1329
|
-
| `<SeoLocalBusiness />` | Physical business locations |
|
|
1330
|
-
| `<SeoCategory />` | Category pages with pagination |
|
|
1331
|
-
| `<SeoTag />` | Tag pages with pagination |
|
|
1332
|
-
| `<SeoAuthor />` | Author profile pages |
|
|
1333
|
-
| `<SeoHowTo />` | How-to guides |
|
|
1334
|
-
| `<SeoReview />` | Review pages |
|
|
1335
|
-
| `<SeoCourse />` | Online courses |
|
|
1336
|
-
| `<SeoRecipe />` | Recipes |
|
|
1337
|
-
| `<SeoJobPosting />` | Job listings |
|
|
1338
|
-
| `<Breadcrumb />` | Breadcrumb navigation with schema |
|
|
1339
|
-
|
|
1340
|
-
> **Note:** `<SeoTag>`, `<SeoAuthor>`, and `<SeoCategory>` accept `pageSuffix` and `titlePrefix` props
|
|
1341
|
-
> for localization (defaults to English). Pass `pageSuffix="صفحة"` for Arabic, etc.
|
|
1342
|
-
|
|
1343
|
-
---
|
|
1344
|
-
|
|
1345
|
-
## Configuration
|
|
1346
|
-
|
|
1347
|
-
### SiteConfig
|
|
1348
|
-
|
|
1349
|
-
```typescript
|
|
1350
|
-
interface SiteConfig {
|
|
1351
|
-
name: string; // Site name (used in title suffix)
|
|
1352
|
-
url: string; // Base URL (no trailing slash)
|
|
1353
|
-
description: string; // Default meta description
|
|
1354
|
-
logo?: string; // Logo URL (used in Organization schema)
|
|
1355
|
-
language?: string; // Locale, e.g. 'en_US' (default: 'ar_SA')
|
|
1356
|
-
twitterHandle?: string; // Twitter @username
|
|
1357
|
-
facebookAppId?: string; // Facebook App ID
|
|
1358
|
-
themeColor?: string; // Mobile browser theme color
|
|
1359
|
-
manifest?: string; // Web app manifest URL
|
|
1360
|
-
socialLinks?: string[]; // Social media profile URLs
|
|
1361
|
-
publisher?: string; // Publisher name
|
|
1362
|
-
}
|
|
1363
|
-
```
|
|
1364
|
-
|
|
1365
|
-
### SEOData
|
|
1366
|
-
|
|
1367
|
-
```typescript
|
|
1368
|
-
interface SEOData {
|
|
1369
|
-
title?: string;
|
|
1370
|
-
description?: string;
|
|
1371
|
-
image?: string;
|
|
1372
|
-
canonical?: string;
|
|
1373
|
-
type?: 'website' | 'article' | 'product' | 'profile' | 'video' | 'faq';
|
|
1374
|
-
robots?: string;
|
|
1375
|
-
noindex?: boolean;
|
|
1376
|
-
keywords?: string[];
|
|
1377
|
-
prev?: string;
|
|
1378
|
-
next?: string;
|
|
1379
|
-
|
|
1380
|
-
// Open Graph
|
|
1381
|
-
ogTitle?: string;
|
|
1382
|
-
ogDescription?: string;
|
|
1383
|
-
ogImage?: string;
|
|
1384
|
-
ogImageWidth?: number; // Default: 1200
|
|
1385
|
-
ogImageHeight?: number; // Default: 630
|
|
1386
|
-
ogImageAlt?: string;
|
|
1387
|
-
ogType?: string;
|
|
1388
|
-
ogLocale?: string;
|
|
1389
|
-
|
|
1390
|
-
// Twitter Cards
|
|
1391
|
-
twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
1392
|
-
twitterTitle?: string;
|
|
1393
|
-
twitterDescription?: string;
|
|
1394
|
-
twitterImage?: string;
|
|
1395
|
-
|
|
1396
|
-
// Article-specific
|
|
1397
|
-
publishedTime?: string;
|
|
1398
|
-
modifiedTime?: string;
|
|
1399
|
-
author?: { name: string; url?: string; image?: string };
|
|
1400
|
-
tags?: string[];
|
|
1401
|
-
section?: string;
|
|
1402
|
-
readingTime?: number;
|
|
1403
|
-
|
|
1404
|
-
// Product-specific
|
|
1405
|
-
product?: {
|
|
1406
|
-
sku?: string;
|
|
1407
|
-
brand?: string;
|
|
1408
|
-
price?: number;
|
|
1409
|
-
currency?: string;
|
|
1410
|
-
availability?: string;
|
|
1411
|
-
rating?: number;
|
|
1412
|
-
reviewCount?: number;
|
|
1413
|
-
};
|
|
1414
|
-
|
|
1415
|
-
// Multilingual
|
|
1416
|
-
alternates?: Array<{ hreflang: string; href: string }>;
|
|
1417
|
-
|
|
1418
|
-
// Performance
|
|
1419
|
-
dnsPrefetch?: string[];
|
|
1420
|
-
preconnect?: string[];
|
|
1421
|
-
prefetch?: string[];
|
|
1422
|
-
preload?: Array<{ href: string; as: string; type?: string }>;
|
|
1423
|
-
|
|
1424
|
-
// Extras
|
|
1425
|
-
whatsappImage?: string;
|
|
1426
|
-
schema?: any;
|
|
1427
|
-
}
|
|
1428
|
-
```
|
|
1429
|
-
|
|
1430
|
-
---
|
|
1431
|
-
|
|
1432
|
-
## Changelog
|
|
1433
|
-
|
|
1434
|
-
### v1.4.0
|
|
1435
|
-
- ✨ **generateHomepageMetadata()** — One-call helper for homepage / landing pages
|
|
1436
|
-
- ✨ **cleanSchema()** — Automatically strips `undefined`/`null` from all JSON-LD output
|
|
1437
|
-
- ✨ **@graph pattern** — `<JsonLd graph />` combines schemas into single `@context`/`@graph` block
|
|
1438
|
-
- ✨ **Multi-image** — Product and Article schemas accept `string | string[]` for images
|
|
1439
|
-
- ✨ **Individual reviews** — Product schema supports `reviews[]` array with author, rating, body
|
|
1440
|
-
- ✨ **MerchantReturnPolicy** — Product schema supports return policy (category, days, method, fees)
|
|
1441
|
-
- ✨ **ShippingDetails** — Product schema supports shipping rate, destination, delivery time, free threshold
|
|
1442
|
-
- ✨ **Product variants** — Automatically generates `AggregateOffer` with `lowPrice`/`highPrice`
|
|
1443
|
-
- ✨ **generateCategoryMetadata()** — One-call helper for category pages with pagination + ItemList
|
|
1444
|
-
- ✨ **WebPage schema** — `generateWebPageSchema()` for general pages
|
|
1445
|
-
- ✨ **CollectionPage schema** — `generateCollectionPageSchema()` for category/archive pages
|
|
1446
|
-
- ✨ **ItemList schema** — `generateItemListSchema()` for product listing pages
|
|
1447
|
-
- ✨ **HowTo schema** — `generateHowToSchema()` with steps, tools, supplies (moved to core)
|
|
1448
|
-
- ✨ **Recipe schema** — `generateRecipeSchema()` (moved to core)
|
|
1449
|
-
- ✨ **JobPosting schema** — `generateJobPostingSchema()` with remote support (moved to core)
|
|
1450
|
-
- ✨ **validateSEO()** — Dev-only console warnings for missing required fields
|
|
1451
|
-
- 🔧 **i18n** — Removed all hardcoded Arabic strings, all labels now configurable via props
|
|
1452
|
-
- 🔧 **Product schema** — Added `gtin`, `mpn`, `condition`, `seller` fields
|
|
1453
|
-
|
|
1454
|
-
### v1.3.0
|
|
1455
|
-
- ✨ Added `<JsonLd>` server-safe component for Next.js App Router
|
|
1456
|
-
- ✨ Added `generateProductMetadata()` — one-call helper for product pages
|
|
1457
|
-
- ✨ Added `generateArticleMetadata()` — one-call helper for article pages
|
|
1458
|
-
- 🔧 Enhanced `toNextMetadata()` with article OG tags, product OG tags, pagination links
|
|
1459
|
-
- 📝 Updated README with SSR best practices
|
|
1460
|
-
|
|
1461
|
-
### v1.2.1
|
|
1462
|
-
- Initial stable release with core functions and Helmet components
|
|
1463
|
-
|
|
1464
|
-
---
|
|
1465
|
-
|
|
1466
|
-
## 💙 Recommended: Outrank — AI-Powered SEO Platform
|
|
1467
|
-
|
|
1468
|
-
Take your SEO to the next level with [**Outrank**](https://outrank.so/?via=shadi) — the AI-powered platform for creating SEO-optimized content at scale.
|
|
1469
|
-
|
|
1470
|
-
- 🚀 **AI Content Generation** — Blog posts, landing pages & more
|
|
1471
|
-
- 📊 **SEO Analysis** — Real-time optimization suggestions
|
|
1472
|
-
- ⚡ **Works great with this package** — Generate content, we handle the technical SEO
|
|
1473
|
-
|
|
1474
|
-
🎁 **Exclusive:** Use code **`SHADI`** for **10% off** your first month.
|
|
1475
|
-
|
|
1476
|
-
> Want to earn too? [Join the affiliate program](https://outrank.so/?via=shadi) — **30% lifetime recurring commission**.
|
|
1477
|
-
|
|
1478
|
-
---
|
|
1479
|
-
|
|
1480
|
-
## License
|
|
1481
|
-
|
|
1482
|
-
MIT
|
|
1
|
+
[](https://www.npmjs.com/package/@masters-ws/react-seo)
|
|
2
|
+
[](https://www.npmjs.com/package/@masters-ws/react-seo)
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
# @masters-ws/react-seo
|
|
6
|
+
|
|
7
|
+
> Professional SEO toolkit for React & Next.js — SSR-first, zero-dependency core, 30+ schema types, and built-in development auditor.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why @masters-ws/react-seo?
|
|
12
|
+
|
|
13
|
+
Most SEO packages inject metadata client-side — after JavaScript loads. Google crawls your page before that. This package fixes that.
|
|
14
|
+
|
|
15
|
+
| Feature | @masters-ws/react-seo | next-seo | react-helmet |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| Zero-dependency core | ✅ | ❌ | ❌ |
|
|
18
|
+
| SSR-first (App Router) | ✅ | ✅ | ❌ |
|
|
19
|
+
| `@graph` JSON-LD | ✅ | ❌ | ❌ |
|
|
20
|
+
| One-call page setup | ✅ | ❌ | ❌ |
|
|
21
|
+
| **Integrated SEO Auditor** | ✅ | ❌ | ❌ |
|
|
22
|
+
| Built-in pagination SEO | ✅ | partial | ❌ |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Next.js App Router (zero dependencies)
|
|
30
|
+
npm install @masters-ws/react-seo
|
|
31
|
+
|
|
32
|
+
# React / Next.js Pages Router
|
|
33
|
+
npm install @masters-ws/react-seo react-helmet-async
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🔍 SEO Auditor (Development Only)
|
|
39
|
+
|
|
40
|
+
This library includes a built-in **SEO Auditor** component designed to speed up your development workflow by providing a real-time analysis of your page's SEO status directly in your browser.
|
|
41
|
+
|
|
42
|
+
- **Real-Time Analysis:** Scans for title/description lengths, H1 counts, missing alt tags, and more.
|
|
43
|
+
- **Visual Previews:** Live mockups for Google Search results and Social Media cards.
|
|
44
|
+
- **Production Safe:** Only renders when `process.env.NODE_ENV === 'development'`.
|
|
45
|
+
|
|
46
|
+
### Usage:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// app/layout.tsx (Next.js App Router)
|
|
50
|
+
import { SEOAuditor } from '@masters-ws/react-seo';
|
|
51
|
+
|
|
52
|
+
export default function RootLayout({ children }) {
|
|
53
|
+
return (
|
|
54
|
+
<html>
|
|
55
|
+
<body>
|
|
56
|
+
{children}
|
|
57
|
+
<SEOAuditor />
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 🚀 Quick Start: Next.js App Router (Recommended)
|
|
67
|
+
|
|
68
|
+
### Product Page — One-Call Setup
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// app/products/[slug]/page.tsx
|
|
72
|
+
import { generateProductMetadata, JsonLd } from '@masters-ws/react-seo/core';
|
|
73
|
+
|
|
74
|
+
const siteConfig = {
|
|
75
|
+
name: "My Store",
|
|
76
|
+
url: "https://store.com",
|
|
77
|
+
logo: "https://store.com/logo.png",
|
|
78
|
+
description: "Best online store",
|
|
79
|
+
language: "en_US",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export async function generateMetadata({ params }) {
|
|
83
|
+
const product = await fetchProduct(params.slug);
|
|
84
|
+
const { metadata } = generateProductMetadata({
|
|
85
|
+
name: product.name,
|
|
86
|
+
description: product.short_description,
|
|
87
|
+
image: [product.main_image, ...product.gallery],
|
|
88
|
+
price: product.price,
|
|
89
|
+
currency: "USD",
|
|
90
|
+
sku: product.sku,
|
|
91
|
+
brand: product.brand?.name,
|
|
92
|
+
availability: product.in_stock
|
|
93
|
+
? "https://schema.org/InStock"
|
|
94
|
+
: "https://schema.org/OutOfStock",
|
|
95
|
+
url: `https://store.com/products/${params.slug}`,
|
|
96
|
+
breadcrumbs: [
|
|
97
|
+
{ name: "Home", item: "https://store.com" },
|
|
98
|
+
{ name: product.category?.name, item: `https://store.com/categories/${product.category?.slug}` },
|
|
99
|
+
{ name: product.name, item: `https://store.com/products/${params.slug}` },
|
|
100
|
+
],
|
|
101
|
+
}, siteConfig);
|
|
102
|
+
|
|
103
|
+
return metadata;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default async function ProductPage({ params }) {
|
|
107
|
+
const product = await fetchProduct(params.slug);
|
|
108
|
+
const { schemas } = generateProductMetadata({ /* same options */ }, siteConfig);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
<JsonLd schema={schemas} graph />
|
|
113
|
+
<ProductDetailClient product={product} />
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**What Google sees in the HTML source:**
|
|
120
|
+
```html
|
|
121
|
+
<title>Product Name | My Store</title>
|
|
122
|
+
<meta name="description" content="..." />
|
|
123
|
+
<meta property="og:title" content="Product Name" />
|
|
124
|
+
<meta property="og:image" content="https://store.com/product.jpg" />
|
|
125
|
+
<link rel="canonical" href="https://store.com/products/my-product" />
|
|
126
|
+
<script type="application/ld+json">
|
|
127
|
+
{
|
|
128
|
+
"@context": "https://schema.org",
|
|
129
|
+
"@graph": [
|
|
130
|
+
{ "@type": "Product", "name": "...", "offers": {...} },
|
|
131
|
+
{ "@type": "BreadcrumbList", "itemListElement": [...] },
|
|
132
|
+
{ "@type": "Organization", "name": "..." },
|
|
133
|
+
{ "@type": "WebSite", "potentialAction": {...} }
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
</script>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Core Functions
|
|
142
|
+
|
|
143
|
+
### One-Call Helpers (Recommended)
|
|
144
|
+
|
|
145
|
+
| Function | Generates |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `generateProductMetadata(product, config)` | Metadata + Product, Breadcrumb, Organization, WebSite schemas |
|
|
148
|
+
| `generateArticleMetadata(article, config)` | Metadata + NewsArticle, Breadcrumb, Organization, WebSite schemas |
|
|
149
|
+
| `generateCategoryMetadata(category, config)` | Metadata + CollectionPage, Breadcrumb, ItemList schemas |
|
|
150
|
+
| `generateHomepageMetadata(input, config)` | Metadata + WebPage, Organization, WebSite, optional LocalBusiness schemas |
|
|
151
|
+
|
|
152
|
+
### Schema Generators
|
|
153
|
+
|
|
154
|
+
| Function | Schema Type |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `generateProductSchema(data)` | `Product` — multi-image, reviews, return policy, shipping, variants |
|
|
157
|
+
| `generateArticleSchema(data, config)` | `NewsArticle` |
|
|
158
|
+
| `generateFAQSchema(questions)` | `FAQPage` |
|
|
159
|
+
| `generateBreadcrumbSchema(items)` | `BreadcrumbList` |
|
|
160
|
+
| `generateLocalBusinessSchema(data)` | `LocalBusiness` |
|
|
161
|
+
| `generateOrganizationSchema(config)` | `Organization` |
|
|
162
|
+
| `generateWebSiteSchema(config)` | `WebSite` with `SearchAction` |
|
|
163
|
+
| `generateWebPageSchema(data, config)` | `WebPage` |
|
|
164
|
+
| `generateHowToSchema(data)` | `HowTo` |
|
|
165
|
+
| `generateRecipeSchema(data)` | `Recipe` |
|
|
166
|
+
| `generateJobPostingSchema(data)` | `JobPosting` |
|
|
167
|
+
| `generateEventSchema(data)` | `Event` |
|
|
168
|
+
| `generateVideoSchema(data)` | `VideoObject` |
|
|
169
|
+
|
|
170
|
+
### Utilities
|
|
171
|
+
|
|
172
|
+
| Function | Description |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `toNextMetadata(data, config)` | Converts SEO data to Next.js Metadata object |
|
|
175
|
+
| `cleanSchema(obj)` | Removes `undefined`/`null` from schema objects |
|
|
176
|
+
| `validateSEO(type, data, fields)` | Dev-only warnings for missing required fields |
|
|
177
|
+
|
|
178
|
+
### `<JsonLd>` Component
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
// Multiple separate <script> tags
|
|
182
|
+
<JsonLd schema={[productSchema, breadcrumbSchema]} />
|
|
183
|
+
|
|
184
|
+
// Single @graph block (Google recommended)
|
|
185
|
+
<JsonLd schema={[productSchema, breadcrumbSchema, orgSchema]} graph />
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Product Schema — Advanced Features
|
|
191
|
+
|
|
192
|
+
### Reviews + Ratings
|
|
193
|
+
```tsx
|
|
194
|
+
generateProductSchema({
|
|
195
|
+
name: "T-Shirt",
|
|
196
|
+
rating: 4.5,
|
|
197
|
+
reviewCount: 128,
|
|
198
|
+
reviews: [
|
|
199
|
+
{ author: "Alice", ratingValue: 5, reviewBody: "Amazing quality!", datePublished: "2024-01-15" },
|
|
200
|
+
{ author: "Bob", ratingValue: 4, reviewBody: "Good value", datePublished: "2024-02-01" },
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Return Policy
|
|
206
|
+
```tsx
|
|
207
|
+
generateProductSchema({
|
|
208
|
+
returnPolicy: {
|
|
209
|
+
returnPolicyCategory: "MerchantReturnFiniteReturnWindow",
|
|
210
|
+
returnWithin: 30,
|
|
211
|
+
returnMethod: "ReturnByMail",
|
|
212
|
+
returnFees: "FreeReturn",
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Shipping
|
|
218
|
+
```tsx
|
|
219
|
+
generateProductSchema({
|
|
220
|
+
shipping: {
|
|
221
|
+
shippingRate: { value: 5.99, currency: "USD" },
|
|
222
|
+
shippingDestination: "US",
|
|
223
|
+
deliveryTime: { minDays: 3, maxDays: 7 },
|
|
224
|
+
freeShippingThreshold: 50,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Product Variants → AggregateOffer
|
|
230
|
+
```tsx
|
|
231
|
+
generateProductSchema({
|
|
232
|
+
variants: [
|
|
233
|
+
{ name: "Small", sku: "TS-S", price: 19.99 },
|
|
234
|
+
{ name: "Medium", sku: "TS-M", price: 22.99 },
|
|
235
|
+
{ name: "Large", sku: "TS-L", price: 24.99 },
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
// → AggregateOffer { lowPrice: 19.99, highPrice: 24.99, offerCount: 3 }
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## React / Pages Router
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
// _app.tsx
|
|
247
|
+
import { SEOProvider } from '@masters-ws/react-seo';
|
|
248
|
+
|
|
249
|
+
export default function App({ Component, pageProps }) {
|
|
250
|
+
return (
|
|
251
|
+
<SEOProvider config={{ name: "My Site", url: "https://mysite.com", description: "..." }}>
|
|
252
|
+
<Component {...pageProps} />
|
|
253
|
+
</SEOProvider>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// pages/products/[id].tsx
|
|
258
|
+
import { SeoProduct } from '@masters-ws/react-seo';
|
|
259
|
+
|
|
260
|
+
export default function ProductPage({ product }) {
|
|
261
|
+
return (
|
|
262
|
+
<>
|
|
263
|
+
<SeoProduct item={{
|
|
264
|
+
name: product.name,
|
|
265
|
+
description: product.description,
|
|
266
|
+
image: product.image,
|
|
267
|
+
price: product.price,
|
|
268
|
+
currency: "USD",
|
|
269
|
+
sku: product.sku,
|
|
270
|
+
}} />
|
|
271
|
+
<main>...</main>
|
|
272
|
+
</>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## ⚠️ SSR vs CSR
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// ✅ Correct — Server Component, Google sees everything
|
|
283
|
+
import { generateProductMetadata, JsonLd } from '@masters-ws/react-seo/core';
|
|
284
|
+
export async function generateMetadata() { /* ... */ }
|
|
285
|
+
export default function Page() {
|
|
286
|
+
return <JsonLd schema={schemas} graph />;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ❌ Wrong — 'use client' means metadata loads after JS
|
|
290
|
+
'use client';
|
|
291
|
+
export default function Page() {
|
|
292
|
+
return <SEO title="..." />; // Google may miss this
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Generating SEO-Optimized Content
|
|
299
|
+
|
|
300
|
+
This package handles technical SEO perfectly. For generating the **actual content** that ranks — blog posts, product descriptions, landing pages — pair it with [**Outrank**](https://outrank.so/?via=shadi), an AI platform built specifically for SEO content at scale.
|
|
301
|
+
|
|
302
|
+
Use code **`SHADI`** for 10% off your first month.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT
|