@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 +1354 -97
- package/dist/chunk-6RBCF5I5.mjs +567 -0
- package/dist/chunk-FFE2ZOOC.mjs +945 -0
- package/dist/chunk-L6YRMB7H.mjs +988 -0
- package/dist/core/index.d.mts +2 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +684 -40
- package/dist/core/index.mjs +29 -3
- package/dist/index-CkB-Wt4c.d.mts +1272 -0
- package/dist/index-CkB-Wt4c.d.ts +1272 -0
- package/dist/index-DEE7ZyDx.d.mts +1193 -0
- package/dist/index-DEE7ZyDx.d.ts +1193 -0
- package/dist/index-Wu9j5oCk.d.mts +721 -0
- package/dist/index-Wu9j5oCk.d.ts +721 -0
- package/dist/index.d.mts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +794 -129
- package/dist/index.mjs +57 -10
- package/package.json +1 -1
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**
|
|
8
|
-
- ✅ **
|
|
9
|
-
- ✅ **
|
|
10
|
-
- ✅
|
|
11
|
-
- ✅ **
|
|
12
|
-
- ✅ **
|
|
13
|
-
- ✅ **
|
|
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
|
-
##
|
|
35
|
+
## 🚀 Quick Start: Next.js App Router (Recommended)
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
### Product Page (One-Call Setup)
|
|
32
38
|
|
|
33
39
|
```tsx
|
|
34
|
-
// app/
|
|
35
|
-
import {
|
|
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
|
|
39
|
-
url: "https://
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
48
|
-
description:
|
|
49
|
-
image:
|
|
50
|
-
type: '
|
|
51
|
-
|
|
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
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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/
|
|
101
|
-
import {
|
|
1121
|
+
// pages/products/[id].tsx
|
|
1122
|
+
import { SeoProduct } from '@masters-ws/react-seo';
|
|
102
1123
|
|
|
103
|
-
export default function
|
|
1124
|
+
export default function ProductPage({ product }) {
|
|
104
1125
|
return (
|
|
105
1126
|
<>
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
description:
|
|
109
|
-
image:
|
|
110
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
| `
|
|
127
|
-
| `generateArticleSchema(data, config)` |
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
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
|
-
|
|
1274
|
+
## Development Warnings
|
|
146
1275
|
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
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
|
-
| `<
|
|
166
|
-
| `<
|
|
167
|
-
| `<
|
|
168
|
-
| `<
|
|
169
|
-
| `<
|
|
170
|
-
| `<
|
|
171
|
-
| `<
|
|
172
|
-
| `<
|
|
173
|
-
| `<
|
|
174
|
-
| `<
|
|
175
|
-
| `<
|
|
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
|
|
1344
|
+
## Configuration
|
|
180
1345
|
|
|
181
1346
|
### SiteConfig
|
|
1347
|
+
|
|
182
1348
|
```typescript
|
|
183
|
-
{
|
|
184
|
-
name: string;
|
|
185
|
-
url: string;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|