@masters-ws/react-seo 1.0.0 → 1.2.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 +142 -91
- package/dist/chunk-AAN7NRZE.mjs +235 -0
- package/dist/chunk-QD5UVA5B.mjs +357 -0
- package/dist/core/index.d.mts +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +399 -0
- package/dist/core/index.mjs +38 -0
- package/dist/index-BEY3UKjK.d.mts +538 -0
- package/dist/index-BEY3UKjK.d.ts +538 -0
- package/dist/index-CGVLxGDj.d.mts +362 -0
- package/dist/index-CGVLxGDj.d.ts +362 -0
- package/dist/index.d.mts +3 -84
- package/dist/index.d.ts +3 -84
- package/dist/index.js +375 -1018
- package/dist/index.mjs +57 -1047
- package/package.json +32 -12
package/README.md
CHANGED
|
@@ -1,138 +1,189 @@
|
|
|
1
1
|
# @masters-ws/react-seo
|
|
2
2
|
|
|
3
|
-
Professional high-performance SEO package for React and Next.js. **
|
|
3
|
+
Professional high-performance SEO package for React and Next.js. **Zero-dependency core** for Next.js App Router + optional Helmet components for universal React support.
|
|
4
4
|
|
|
5
5
|
## Key Features
|
|
6
6
|
|
|
7
|
-
- ✅ **
|
|
8
|
-
- ✅ **
|
|
9
|
-
- ✅ **
|
|
10
|
-
- ✅ **
|
|
11
|
-
- ✅ **
|
|
12
|
-
- ✅ **
|
|
13
|
-
- ✅ **
|
|
14
|
-
- ✅ **Mobile Optimized** - Theme color, Apple mobile web app capability, and manifest support.
|
|
15
|
-
- ✅ **Analytics Integration** - Built-in hooks for GA4, GTM, and Facebook Pixel.
|
|
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
|
|
16
14
|
|
|
17
15
|
## Installation
|
|
18
16
|
|
|
17
|
+
### For Next.js App Router (Zero Dependencies):
|
|
18
|
+
```bash
|
|
19
|
+
npm install @masters-ws/react-seo
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### For React / Next.js Pages Router (With Components):
|
|
19
23
|
```bash
|
|
20
24
|
npm install @masters-ws/react-seo react-helmet-async
|
|
21
25
|
```
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Usage: Next.js App Router (Recommended)
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
Use the **core functions** directly in your Server Components. No Helmet needed!
|
|
26
32
|
|
|
27
33
|
```tsx
|
|
28
|
-
// page.tsx
|
|
29
|
-
import { toNextMetadata } from '@
|
|
34
|
+
// app/news/[slug]/page.tsx
|
|
35
|
+
import { toNextMetadata, generateArticleSchema } from '@masters-ws/react-seo';
|
|
30
36
|
|
|
31
|
-
const
|
|
37
|
+
const siteConfig = {
|
|
38
|
+
name: "My News Site",
|
|
39
|
+
url: "https://mysite.com"
|
|
40
|
+
};
|
|
32
41
|
|
|
42
|
+
// This runs on the server - instant SEO!
|
|
33
43
|
export async function generateMetadata({ params }) {
|
|
34
|
-
const post = await getPost(params.
|
|
44
|
+
const post = await getPost(params.slug);
|
|
35
45
|
|
|
36
46
|
return toNextMetadata({
|
|
37
47
|
title: post.title,
|
|
38
48
|
description: post.excerpt,
|
|
49
|
+
image: post.cover,
|
|
39
50
|
type: 'article',
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
publishedTime: post.date,
|
|
43
|
-
readingTime: 5 // Optional: shows in Twitter preview
|
|
44
|
-
}, config);
|
|
51
|
+
publishedTime: post.date
|
|
52
|
+
}, siteConfig);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
export default function
|
|
48
|
-
|
|
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 }
|
|
64
|
+
}, siteConfig);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<script
|
|
69
|
+
type="application/ld+json"
|
|
70
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
|
|
71
|
+
/>
|
|
72
|
+
<article>...</article>
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
49
75
|
}
|
|
50
76
|
```
|
|
51
77
|
|
|
52
|
-
|
|
78
|
+
---
|
|
53
79
|
|
|
54
|
-
|
|
55
|
-
|
|
80
|
+
## Usage: React / Pages Router (With Components)
|
|
81
|
+
|
|
82
|
+
If you prefer the component approach or are using React without Next.js:
|
|
56
83
|
|
|
57
84
|
```tsx
|
|
58
|
-
|
|
85
|
+
// _app.tsx
|
|
86
|
+
import { SEOProvider } from '@masters-ws/react-seo';
|
|
59
87
|
|
|
60
|
-
const
|
|
61
|
-
name: "My Site",
|
|
62
|
-
url: "https://mysite.com",
|
|
63
|
-
googleAnalyticsId: "G-XXXXXXXXXX",
|
|
64
|
-
themeColor: "#007bff"
|
|
65
|
-
};
|
|
88
|
+
const config = { name: "My Site", url: "https://mysite.com" };
|
|
66
89
|
|
|
67
|
-
function App({
|
|
90
|
+
export default function App({ Component, pageProps }) {
|
|
68
91
|
return (
|
|
69
|
-
<SEOProvider config={
|
|
70
|
-
{
|
|
92
|
+
<SEOProvider config={config}>
|
|
93
|
+
<Component {...pageProps} />
|
|
71
94
|
</SEOProvider>
|
|
72
95
|
);
|
|
73
96
|
}
|
|
74
97
|
```
|
|
75
98
|
|
|
76
|
-
### 2. Specialized Components
|
|
77
|
-
|
|
78
|
-
#### News Article
|
|
79
99
|
```tsx
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
/>
|
|
100
|
+
// pages/article.tsx
|
|
101
|
+
import { SeoArticle } from '@masters-ws/react-seo';
|
|
102
|
+
|
|
103
|
+
export default function ArticlePage({ post }) {
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
<SeoArticle article={{
|
|
107
|
+
title: post.title,
|
|
108
|
+
description: post.excerpt,
|
|
109
|
+
image: post.cover,
|
|
110
|
+
publishedAt: post.date
|
|
111
|
+
}} />
|
|
112
|
+
<article>...</article>
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
92
116
|
```
|
|
93
117
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Core Functions Reference
|
|
121
|
+
|
|
122
|
+
All functions are pure (no side effects) and work in any environment:
|
|
123
|
+
|
|
124
|
+
| Function | Description |
|
|
125
|
+
| :--- | :--- |
|
|
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
|
+
| `generatePaginationLinks(url, page, total)` | Returns prev/next/canonical URLs |
|
|
135
|
+
| `generateOrganizationSchema(config)` | Creates Organization JSON-LD |
|
|
136
|
+
| `generateWebSiteSchema(config)` | Creates WebSite JSON-LD with SearchAction |
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Components Reference (Requires react-helmet-async)
|
|
141
|
+
|
|
142
|
+
| Component | Description |
|
|
143
|
+
| :--- | :--- |
|
|
144
|
+
| `<SEO />` | Main component for meta tags and schemas |
|
|
145
|
+
| `<SeoArticle />` | For news articles and blog posts |
|
|
146
|
+
| `<SeoProduct />` | For e-commerce products |
|
|
147
|
+
| `<SeoFAQ />` | For FAQ pages |
|
|
148
|
+
| `<SeoVideo />` | For video content |
|
|
149
|
+
| `<SeoEvent />` | For events and conferences |
|
|
150
|
+
| `<SeoLocalBusiness />` | For physical business locations |
|
|
151
|
+
| `<SeoCategory />` | For category pages with pagination |
|
|
152
|
+
| `<SeoTag />` | For tag pages with pagination |
|
|
153
|
+
| `<SeoAuthor />` | For author profile pages |
|
|
154
|
+
| `<Breadcrumb />` | For breadcrumb navigation with schema |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Configuration Options
|
|
159
|
+
|
|
160
|
+
### SiteConfig
|
|
161
|
+
```typescript
|
|
162
|
+
{
|
|
163
|
+
name: string; // Site name
|
|
164
|
+
url: string; // Base URL
|
|
165
|
+
logo?: string; // Logo URL
|
|
166
|
+
language?: string; // Default: 'ar'
|
|
167
|
+
twitterHandle?: string; // @username
|
|
168
|
+
themeColor?: string; // Mobile theme color
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### SEOData
|
|
173
|
+
```typescript
|
|
174
|
+
{
|
|
175
|
+
title?: string;
|
|
176
|
+
description?: string;
|
|
177
|
+
image?: string;
|
|
178
|
+
canonical?: string;
|
|
179
|
+
noindex?: boolean; // Sets robots to noindex
|
|
180
|
+
type?: 'website' | 'article' | 'product';
|
|
181
|
+
alternates?: Array<{ hreflang: string; href: string }>;
|
|
182
|
+
// ... and more
|
|
183
|
+
}
|
|
104
184
|
```
|
|
105
185
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
### SiteConfig Props
|
|
109
|
-
| Prop | Type | Description |
|
|
110
|
-
| :--- | :--- | :--- |
|
|
111
|
-
| `name` | `string` | Site name (used in titles and schemas) |
|
|
112
|
-
| `url` | `string` | Base URL of the site |
|
|
113
|
-
| `logo` | `string` | URL to the site logo |
|
|
114
|
-
| `language` | `string` | Main language (e.g., 'ar', 'en') |
|
|
115
|
-
| `googleAnalyticsId`| `string` | GA4 Measurement ID |
|
|
116
|
-
| `themeColor` | `string` | Browser theme color (mobile) |
|
|
117
|
-
|
|
118
|
-
### SEOData Props (Common)
|
|
119
|
-
| Prop | Type | Description |
|
|
120
|
-
| :--- | :--- | :--- |
|
|
121
|
-
| `title` | `string` | Page title (auto-appends Site name) |
|
|
122
|
-
| `noindex` | `boolean` | Sets robots to noindex and removes canonical |
|
|
123
|
-
| `alternates` | `Array` | List of `{hreflang, href}` for multilingual |
|
|
124
|
-
| `dnsPrefetch` | `string[]` | List of domains to prefetch |
|
|
125
|
-
| `readingTime` | `number` | Reading time in minutes for Twitter Cards |
|
|
126
|
-
|
|
127
|
-
## Advanced Schemas List
|
|
128
|
-
Use these components for specific SEO needs:
|
|
129
|
-
- `<SeoVideo />` - For video content.
|
|
130
|
-
- `<SeoEvent />` - For conferences/events.
|
|
131
|
-
- `<SeoLocalBusiness />` - For physical stores.
|
|
132
|
-
- `<SeoHowTo />` - For tutorials.
|
|
133
|
-
- `<SeoReview />` - For product ratings.
|
|
134
|
-
- `<SeoJobPosting />` - For hiring.
|
|
135
|
-
- `<SeoRecipe />` - For cooking sites.
|
|
186
|
+
---
|
|
136
187
|
|
|
137
188
|
## License
|
|
138
189
|
MIT
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// src/core/schemas.ts
|
|
2
|
+
function generateOrganizationSchema(config) {
|
|
3
|
+
return {
|
|
4
|
+
"@context": "https://schema.org",
|
|
5
|
+
"@type": "Organization",
|
|
6
|
+
"name": config.name,
|
|
7
|
+
"url": config.url,
|
|
8
|
+
"logo": config.logo,
|
|
9
|
+
"sameAs": config.socialLinks || []
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function generateWebSiteSchema(config) {
|
|
13
|
+
return {
|
|
14
|
+
"@context": "https://schema.org",
|
|
15
|
+
"@type": "WebSite",
|
|
16
|
+
"name": config.name,
|
|
17
|
+
"url": config.url,
|
|
18
|
+
"potentialAction": {
|
|
19
|
+
"@type": "SearchAction",
|
|
20
|
+
"target": `${config.url}/search?q={search_term_string}`,
|
|
21
|
+
"query-input": "required name=search_term_string"
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function generateArticleSchema(data, config) {
|
|
26
|
+
const org = generateOrganizationSchema(config);
|
|
27
|
+
return {
|
|
28
|
+
"@context": "https://schema.org",
|
|
29
|
+
"@type": "NewsArticle",
|
|
30
|
+
"headline": data.title,
|
|
31
|
+
"description": data.description,
|
|
32
|
+
"image": data.image || config.logo,
|
|
33
|
+
"datePublished": data.publishedTime,
|
|
34
|
+
"dateModified": data.modifiedTime || data.publishedTime,
|
|
35
|
+
"mainEntityOfPage": data.url,
|
|
36
|
+
"author": data.author ? {
|
|
37
|
+
"@type": "Person",
|
|
38
|
+
"name": data.author.name,
|
|
39
|
+
"url": data.author.url
|
|
40
|
+
} : org,
|
|
41
|
+
"publisher": org
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function generateProductSchema(data) {
|
|
45
|
+
return {
|
|
46
|
+
"@context": "https://schema.org",
|
|
47
|
+
"@type": "Product",
|
|
48
|
+
"name": data.name,
|
|
49
|
+
"description": data.description,
|
|
50
|
+
"image": data.image,
|
|
51
|
+
"sku": data.sku,
|
|
52
|
+
"brand": data.brand ? { "@type": "Brand", "name": data.brand } : void 0,
|
|
53
|
+
"offers": {
|
|
54
|
+
"@type": "Offer",
|
|
55
|
+
"url": data.url,
|
|
56
|
+
"priceCurrency": data.currency || "USD",
|
|
57
|
+
"price": data.price,
|
|
58
|
+
"availability": data.availability || "https://schema.org/InStock"
|
|
59
|
+
},
|
|
60
|
+
"aggregateRating": data.rating ? {
|
|
61
|
+
"@type": "AggregateRating",
|
|
62
|
+
"ratingValue": data.rating,
|
|
63
|
+
"reviewCount": data.reviewCount || 1
|
|
64
|
+
} : void 0
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function generateFAQSchema(questions) {
|
|
68
|
+
return {
|
|
69
|
+
"@context": "https://schema.org",
|
|
70
|
+
"@type": "FAQPage",
|
|
71
|
+
"mainEntity": questions.map((item) => ({
|
|
72
|
+
"@type": "Question",
|
|
73
|
+
"name": item.q,
|
|
74
|
+
"acceptedAnswer": {
|
|
75
|
+
"@type": "Answer",
|
|
76
|
+
"text": item.a
|
|
77
|
+
}
|
|
78
|
+
}))
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function generateBreadcrumbSchema(items) {
|
|
82
|
+
return {
|
|
83
|
+
"@context": "https://schema.org",
|
|
84
|
+
"@type": "BreadcrumbList",
|
|
85
|
+
"itemListElement": items.map((item, index) => ({
|
|
86
|
+
"@type": "ListItem",
|
|
87
|
+
"position": index + 1,
|
|
88
|
+
"name": item.name,
|
|
89
|
+
"item": item.item
|
|
90
|
+
}))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function generateVideoSchema(data) {
|
|
94
|
+
return {
|
|
95
|
+
"@context": "https://schema.org",
|
|
96
|
+
"@type": "VideoObject",
|
|
97
|
+
"name": data.name,
|
|
98
|
+
"description": data.description,
|
|
99
|
+
"thumbnailUrl": data.thumbnailUrl,
|
|
100
|
+
"uploadDate": data.uploadDate,
|
|
101
|
+
"duration": data.duration,
|
|
102
|
+
"contentUrl": data.contentUrl,
|
|
103
|
+
"embedUrl": data.embedUrl
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function generateEventSchema(data) {
|
|
107
|
+
const isOnline = data.location && "url" in data.location;
|
|
108
|
+
return {
|
|
109
|
+
"@context": "https://schema.org",
|
|
110
|
+
"@type": "Event",
|
|
111
|
+
"name": data.name,
|
|
112
|
+
"description": data.description,
|
|
113
|
+
"startDate": data.startDate,
|
|
114
|
+
"endDate": data.endDate,
|
|
115
|
+
"eventAttendanceMode": isOnline ? "https://schema.org/OnlineEventAttendanceMode" : "https://schema.org/OfflineEventAttendanceMode",
|
|
116
|
+
"location": isOnline ? {
|
|
117
|
+
"@type": "VirtualLocation",
|
|
118
|
+
"url": data.location.url
|
|
119
|
+
} : data.location ? {
|
|
120
|
+
"@type": "Place",
|
|
121
|
+
"name": data.location.name,
|
|
122
|
+
"address": data.location.address
|
|
123
|
+
} : void 0,
|
|
124
|
+
"image": data.image,
|
|
125
|
+
"offers": data.offers ? {
|
|
126
|
+
"@type": "Offer",
|
|
127
|
+
"price": data.offers.price,
|
|
128
|
+
"priceCurrency": data.offers.currency,
|
|
129
|
+
"url": data.offers.url
|
|
130
|
+
} : void 0
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function generateLocalBusinessSchema(data) {
|
|
134
|
+
return {
|
|
135
|
+
"@context": "https://schema.org",
|
|
136
|
+
"@type": "LocalBusiness",
|
|
137
|
+
"name": data.name,
|
|
138
|
+
"description": data.description,
|
|
139
|
+
"image": data.image,
|
|
140
|
+
"telephone": data.telephone,
|
|
141
|
+
"address": {
|
|
142
|
+
"@type": "PostalAddress",
|
|
143
|
+
"streetAddress": data.address.street,
|
|
144
|
+
"addressLocality": data.address.city,
|
|
145
|
+
"addressRegion": data.address.region,
|
|
146
|
+
"postalCode": data.address.postalCode,
|
|
147
|
+
"addressCountry": data.address.country
|
|
148
|
+
},
|
|
149
|
+
"geo": data.geo ? {
|
|
150
|
+
"@type": "GeoCoordinates",
|
|
151
|
+
"latitude": data.geo.lat,
|
|
152
|
+
"longitude": data.geo.lng
|
|
153
|
+
} : void 0,
|
|
154
|
+
"openingHours": data.openingHours,
|
|
155
|
+
"priceRange": data.priceRange
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/core/metadata.ts
|
|
160
|
+
function toNextMetadata(props, config) {
|
|
161
|
+
const title = props.title ? `${props.title} | ${config.name}` : config.name;
|
|
162
|
+
const description = props.description || config.description;
|
|
163
|
+
const url = props.canonical || config.url;
|
|
164
|
+
const image = props.image || config.logo;
|
|
165
|
+
const metadata = {
|
|
166
|
+
title,
|
|
167
|
+
description,
|
|
168
|
+
keywords: props.keywords,
|
|
169
|
+
robots: props.noindex ? "noindex, nofollow" : props.robots || "index, follow",
|
|
170
|
+
alternates: {
|
|
171
|
+
canonical: props.noindex ? void 0 : url
|
|
172
|
+
},
|
|
173
|
+
openGraph: {
|
|
174
|
+
title: props.ogTitle || title,
|
|
175
|
+
description: props.ogDescription || description,
|
|
176
|
+
url,
|
|
177
|
+
siteName: config.name,
|
|
178
|
+
images: props.ogImage || image ? [{ url: props.ogImage || image }] : [],
|
|
179
|
+
type: props.ogType || (props.type === "article" ? "article" : "website"),
|
|
180
|
+
locale: props.ogLocale || config.language || "ar_SA"
|
|
181
|
+
},
|
|
182
|
+
twitter: {
|
|
183
|
+
card: props.twitterCard || "summary_large_image",
|
|
184
|
+
title: props.twitterTitle || title,
|
|
185
|
+
description: props.twitterDescription || description,
|
|
186
|
+
images: props.twitterImage || image ? [props.twitterImage || image] : [],
|
|
187
|
+
site: config.twitterHandle,
|
|
188
|
+
creator: config.twitterHandle
|
|
189
|
+
},
|
|
190
|
+
other: {}
|
|
191
|
+
};
|
|
192
|
+
if (props.alternates && props.alternates.length > 0) {
|
|
193
|
+
const languages = {};
|
|
194
|
+
props.alternates.forEach((alt) => {
|
|
195
|
+
languages[alt.hreflang] = alt.href;
|
|
196
|
+
});
|
|
197
|
+
metadata.alternates.languages = languages;
|
|
198
|
+
}
|
|
199
|
+
metadata.appleWebApp = {
|
|
200
|
+
capable: true,
|
|
201
|
+
title: config.name,
|
|
202
|
+
statusBarStyle: "default"
|
|
203
|
+
};
|
|
204
|
+
if (config.themeColor) metadata.themeColor = config.themeColor;
|
|
205
|
+
if (config.manifest) metadata.manifest = config.manifest;
|
|
206
|
+
return metadata;
|
|
207
|
+
}
|
|
208
|
+
function generatePaginationLinks(baseUrl, currentPage, totalPages) {
|
|
209
|
+
const hasNext = currentPage < totalPages;
|
|
210
|
+
const hasPrev = currentPage > 1;
|
|
211
|
+
const cleanBase = baseUrl.split("?")[0];
|
|
212
|
+
return {
|
|
213
|
+
next: hasNext ? `${cleanBase}?page=${currentPage + 1}` : void 0,
|
|
214
|
+
prev: hasPrev ? currentPage === 2 ? cleanBase : `${cleanBase}?page=${currentPage - 1}` : void 0,
|
|
215
|
+
canonical: currentPage === 1 ? cleanBase : `${cleanBase}?page=${currentPage}`
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function generatePaginatedTitle(title, page, suffix = "\u0635\u0641\u062D\u0629") {
|
|
219
|
+
return page > 1 ? `${title} - ${suffix} ${page}` : title;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export {
|
|
223
|
+
generateOrganizationSchema,
|
|
224
|
+
generateWebSiteSchema,
|
|
225
|
+
generateArticleSchema,
|
|
226
|
+
generateProductSchema,
|
|
227
|
+
generateFAQSchema,
|
|
228
|
+
generateBreadcrumbSchema,
|
|
229
|
+
generateVideoSchema,
|
|
230
|
+
generateEventSchema,
|
|
231
|
+
generateLocalBusinessSchema,
|
|
232
|
+
toNextMetadata,
|
|
233
|
+
generatePaginationLinks,
|
|
234
|
+
generatePaginatedTitle
|
|
235
|
+
};
|