@masters-ws/react-seo 1.2.1 → 1.4.0

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