react-ssr-seo-toolkit 1.0.2
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/LICENSE +21 -0
- package/README.md +1186 -0
- package/dist/chunk-63ETSZTD.cjs +3 -0
- package/dist/chunk-63ETSZTD.cjs.map +1 -0
- package/dist/chunk-ES4OXVOR.js +3 -0
- package/dist/chunk-ES4OXVOR.js.map +1 -0
- package/dist/chunk-QBHCTDUJ.cjs +2 -0
- package/dist/chunk-QBHCTDUJ.cjs.map +1 -0
- package/dist/chunk-YMCW2G4X.js +2 -0
- package/dist/chunk-YMCW2G4X.js.map +1 -0
- package/dist/components.cjs +2 -0
- package/dist/components.cjs.map +1 -0
- package/dist/components.d.cts +3 -0
- package/dist/components.d.ts +3 -0
- package/dist/components.js +2 -0
- package/dist/components.js.map +1 -0
- package/dist/index-DAGfo2Fc.d.ts +32 -0
- package/dist/index-Dr2yktvz.d.cts +136 -0
- package/dist/index-Dr2yktvz.d.ts +136 -0
- package/dist/index-RBSUcdqN.d.cts +32 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +103 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.cjs +2 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +15 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -0
- package/package.json +114 -0
package/README.md
ADDED
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<br />
|
|
4
|
+
|
|
5
|
+
<img src="https://img.shields.io/badge/react--ssr--seo-v1.0.1-000000?style=for-the-badge&labelColor=000000" alt="react-ssr-seo" />
|
|
6
|
+
|
|
7
|
+
<br />
|
|
8
|
+
<br />
|
|
9
|
+
|
|
10
|
+
# `react-ssr-seo`
|
|
11
|
+
|
|
12
|
+
### The Complete SEO Toolkit for React SSR Applications
|
|
13
|
+
|
|
14
|
+
<br />
|
|
15
|
+
|
|
16
|
+
[](https://www.npmjs.com/package/react-ssr-seo-toolkit)
|
|
17
|
+
|
|
18
|
+
[](https://www.typescriptlang.org/)
|
|
19
|
+
|
|
20
|
+
[](./LICENSE)
|
|
21
|
+
|
|
22
|
+
[](https://bundlephobia.com/package/react-ssr-seo-toolkit)
|
|
23
|
+
|
|
24
|
+
<br />
|
|
25
|
+
|
|
26
|
+
**Meta Tags** • **Open Graph** • **Twitter Cards** • **JSON-LD** • **Canonical URLs** • **Hreflang** • **Robots**
|
|
27
|
+
|
|
28
|
+
All in one package. Zero dependencies. Fully typed. SSR-safe.
|
|
29
|
+
|
|
30
|
+
<br />
|
|
31
|
+
|
|
32
|
+
[Live Demo](https://react-ssr-seo.tonmoykhan.site/) | [Get Started](#-get-started) | [Examples](#-real-world-examples) | [API](#-api-reference) | [Frameworks](#-framework-integration)
|
|
33
|
+
|
|
34
|
+
<br />
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<br />
|
|
41
|
+
|
|
42
|
+
## Why This Package?
|
|
43
|
+
|
|
44
|
+
<table>
|
|
45
|
+
<tr>
|
|
46
|
+
<td width="50%">
|
|
47
|
+
|
|
48
|
+
### The Problem
|
|
49
|
+
|
|
50
|
+
- Most SEO packages are **locked to Next.js**
|
|
51
|
+
- Many rely on **browser-only APIs** (`window`, `document`)
|
|
52
|
+
- JSON-LD usually needs a **separate package**
|
|
53
|
+
- Hard to get **type safety** across meta tags
|
|
54
|
+
- Hydration mismatches in SSR
|
|
55
|
+
|
|
56
|
+
</td>
|
|
57
|
+
<td width="50%">
|
|
58
|
+
|
|
59
|
+
### The Solution
|
|
60
|
+
|
|
61
|
+
- **Framework-agnostic** — works everywhere
|
|
62
|
+
- **Zero browser globals** — fully SSR-safe
|
|
63
|
+
- **JSON-LD built-in** — Article, Product, FAQ, Breadcrumb, Organization, Website
|
|
64
|
+
- **Full TypeScript** — every prop, every config
|
|
65
|
+
- **Deterministic output** — no hydration issues
|
|
66
|
+
|
|
67
|
+
</td>
|
|
68
|
+
</tr>
|
|
69
|
+
</table>
|
|
70
|
+
|
|
71
|
+
<br />
|
|
72
|
+
|
|
73
|
+
## Works With
|
|
74
|
+
|
|
75
|
+
<div align="center">
|
|
76
|
+
|
|
77
|
+
| | Framework | Integration |
|
|
78
|
+
|:---:|---|---|
|
|
79
|
+
| <img src="https://img.shields.io/badge/-Next.js-000?style=flat-square&logo=nextdotjs" /> | **Next.js App Router** | `generateMetadata()` + `safeJsonLdSerialize()` |
|
|
80
|
+
| <img src="https://img.shields.io/badge/-Next.js-000?style=flat-square&logo=nextdotjs" /> | **Next.js Pages Router** | `<SEOHead>` inside `next/head` |
|
|
81
|
+
| <img src="https://img.shields.io/badge/-React_Router-CA4245?style=flat-square&logo=reactrouter&logoColor=white" /> | **React Router 7 SSR** | `<SEOHead>` in root component |
|
|
82
|
+
| <img src="https://img.shields.io/badge/-Express-000?style=flat-square&logo=express" /> | **Express + React SSR** | `<SEOHead>` in `renderToString()` |
|
|
83
|
+
| <img src="https://img.shields.io/badge/-Remix-000?style=flat-square&logo=remix" /> | **Remix / Astro / Solid** | Pure utility functions (no React needed) |
|
|
84
|
+
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<br />
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
<br />
|
|
92
|
+
|
|
93
|
+
## Get Started
|
|
94
|
+
|
|
95
|
+
### 1. Install
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm install react-ssr-seo-toolkit
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# or
|
|
103
|
+
pnpm add react-ssr-seo-toolkit # yarn add react-ssr-seo-toolkit # bun add react-ssr-seo-toolkit
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
> **Requires:** `react >= 18.0.0` as peer dependency
|
|
107
|
+
|
|
108
|
+
<br />
|
|
109
|
+
|
|
110
|
+
### 2. Create Site Config (once)
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { createSEOConfig } from "react-ssr-seo-toolkit";
|
|
114
|
+
|
|
115
|
+
const siteConfig = createSEOConfig({
|
|
116
|
+
titleTemplate: "%s | MySite", // auto-appends " | MySite" to every page title
|
|
117
|
+
openGraph: { siteName: "MySite", type: "website" },
|
|
118
|
+
twitter: { card: "summary_large_image", site: "@mysite" },
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
<br />
|
|
123
|
+
|
|
124
|
+
### 3. Add to Any Page
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { SEOHead, mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
|
|
128
|
+
|
|
129
|
+
function AboutPage() {
|
|
130
|
+
const seo = mergeSEOConfig(siteConfig, {
|
|
131
|
+
title: "About Us",
|
|
132
|
+
description: "Learn about our company and mission.",
|
|
133
|
+
canonical: buildCanonicalUrl("https://mysite.com", "/about"),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<html>
|
|
138
|
+
<head>
|
|
139
|
+
<SEOHead {...seo} />
|
|
140
|
+
{/* Renders: <title>, <meta>, <link>, <script type="application/ld+json"> */}
|
|
141
|
+
</head>
|
|
142
|
+
<body>
|
|
143
|
+
<h1>About Us</h1>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Done.** That's all you need for basic SEO. Keep reading for real-world examples.
|
|
151
|
+
|
|
152
|
+
<br />
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
<br />
|
|
157
|
+
|
|
158
|
+
## Real-World Examples
|
|
159
|
+
|
|
160
|
+
> Every example below is **copy-paste ready**. Just change the URLs and content.
|
|
161
|
+
|
|
162
|
+
<br />
|
|
163
|
+
|
|
164
|
+
### Blog / Article Page
|
|
165
|
+
|
|
166
|
+
<details open>
|
|
167
|
+
<summary><strong>Click to expand full example</strong></summary>
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import {
|
|
171
|
+
SEOHead, JsonLd,
|
|
172
|
+
createSEOConfig, mergeSEOConfig, buildCanonicalUrl,
|
|
173
|
+
createArticleSchema, createBreadcrumbSchema,
|
|
174
|
+
} from "react-ssr-seo-toolkit";
|
|
175
|
+
|
|
176
|
+
// Site config (create once, reuse everywhere)
|
|
177
|
+
const siteConfig = createSEOConfig({
|
|
178
|
+
titleTemplate: "%s | My Blog",
|
|
179
|
+
openGraph: { siteName: "My Blog", type: "website" },
|
|
180
|
+
twitter: { card: "summary_large_image", site: "@myblog" },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
function BlogPostPage() {
|
|
184
|
+
// ── Page SEO ──────────────────────────────────────────────
|
|
185
|
+
const seo = mergeSEOConfig(siteConfig, {
|
|
186
|
+
title: "How to Build an SSR App",
|
|
187
|
+
description: "A complete guide to building server-rendered React apps with proper SEO.",
|
|
188
|
+
canonical: buildCanonicalUrl("https://myblog.com", "/blog/ssr-guide"),
|
|
189
|
+
openGraph: {
|
|
190
|
+
title: "How to Build an SSR App",
|
|
191
|
+
description: "A complete guide to SSR with React.",
|
|
192
|
+
type: "article",
|
|
193
|
+
url: "https://myblog.com/blog/ssr-guide",
|
|
194
|
+
images: [{
|
|
195
|
+
url: "https://myblog.com/images/ssr-guide.jpg",
|
|
196
|
+
width: 1200, height: 630,
|
|
197
|
+
alt: "SSR Guide Cover",
|
|
198
|
+
}],
|
|
199
|
+
},
|
|
200
|
+
twitter: {
|
|
201
|
+
title: "How to Build an SSR App",
|
|
202
|
+
creator: "@authorhandle",
|
|
203
|
+
image: "https://myblog.com/images/ssr-guide.jpg",
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ── Structured Data ───────────────────────────────────────
|
|
208
|
+
const article = createArticleSchema({
|
|
209
|
+
headline: "How to Build an SSR App",
|
|
210
|
+
url: "https://myblog.com/blog/ssr-guide",
|
|
211
|
+
description: "A complete guide to SSR with React.",
|
|
212
|
+
datePublished: "2025-06-15",
|
|
213
|
+
dateModified: "2025-07-01",
|
|
214
|
+
author: [
|
|
215
|
+
{ name: "Jane Doe", url: "https://myblog.com/authors/jane" },
|
|
216
|
+
{ name: "John Smith" },
|
|
217
|
+
],
|
|
218
|
+
publisher: { name: "My Blog", logo: "https://myblog.com/logo.png" },
|
|
219
|
+
images: ["https://myblog.com/images/ssr-guide.jpg"],
|
|
220
|
+
section: "Technology",
|
|
221
|
+
keywords: ["React", "SSR", "SEO"],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const breadcrumbs = createBreadcrumbSchema([
|
|
225
|
+
{ name: "Home", url: "https://myblog.com" },
|
|
226
|
+
{ name: "Blog", url: "https://myblog.com/blog" },
|
|
227
|
+
{ name: "How to Build an SSR App", url: "https://myblog.com/blog/ssr-guide" },
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
// ── Render ────────────────────────────────────────────────
|
|
231
|
+
return (
|
|
232
|
+
<html>
|
|
233
|
+
<head>
|
|
234
|
+
<SEOHead {...seo} />
|
|
235
|
+
<JsonLd data={article} />
|
|
236
|
+
<JsonLd data={breadcrumbs} />
|
|
237
|
+
</head>
|
|
238
|
+
<body>
|
|
239
|
+
<article>
|
|
240
|
+
<h1>How to Build an SSR App</h1>
|
|
241
|
+
<p>Your article content here...</p>
|
|
242
|
+
</article>
|
|
243
|
+
</body>
|
|
244
|
+
</html>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
</details>
|
|
250
|
+
|
|
251
|
+
<details>
|
|
252
|
+
<summary><strong>See the HTML output this generates</strong></summary>
|
|
253
|
+
|
|
254
|
+
```html
|
|
255
|
+
<head>
|
|
256
|
+
<!-- Basic -->
|
|
257
|
+
<title>How to Build an SSR App | My Blog</title>
|
|
258
|
+
<meta name="description" content="A complete guide to building server-rendered React apps..." />
|
|
259
|
+
<link rel="canonical" href="https://myblog.com/blog/ssr-guide" />
|
|
260
|
+
<meta name="robots" content="index, follow" />
|
|
261
|
+
|
|
262
|
+
<!-- Open Graph (Facebook, LinkedIn, etc.) -->
|
|
263
|
+
<meta property="og:title" content="How to Build an SSR App" />
|
|
264
|
+
<meta property="og:description" content="A complete guide to SSR with React." />
|
|
265
|
+
<meta property="og:type" content="article" />
|
|
266
|
+
<meta property="og:url" content="https://myblog.com/blog/ssr-guide" />
|
|
267
|
+
<meta property="og:site_name" content="My Blog" />
|
|
268
|
+
<meta property="og:image" content="https://myblog.com/images/ssr-guide.jpg" />
|
|
269
|
+
<meta property="og:image:width" content="1200" />
|
|
270
|
+
<meta property="og:image:height" content="630" />
|
|
271
|
+
<meta property="og:image:alt" content="SSR Guide Cover" />
|
|
272
|
+
|
|
273
|
+
<!-- Twitter Card -->
|
|
274
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
275
|
+
<meta name="twitter:site" content="@myblog" />
|
|
276
|
+
<meta name="twitter:creator" content="@authorhandle" />
|
|
277
|
+
<meta name="twitter:title" content="How to Build an SSR App" />
|
|
278
|
+
<meta name="twitter:image" content="https://myblog.com/images/ssr-guide.jpg" />
|
|
279
|
+
|
|
280
|
+
<!-- JSON-LD: Article -->
|
|
281
|
+
<script type="application/ld+json">
|
|
282
|
+
{"@context":"https://schema.org","@type":"Article","headline":"How to Build an SSR App",...}
|
|
283
|
+
</script>
|
|
284
|
+
|
|
285
|
+
<!-- JSON-LD: Breadcrumbs -->
|
|
286
|
+
<script type="application/ld+json">
|
|
287
|
+
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[...]}
|
|
288
|
+
</script>
|
|
289
|
+
</head>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
</details>
|
|
293
|
+
|
|
294
|
+
<br />
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
<br />
|
|
299
|
+
|
|
300
|
+
### E-Commerce Product Page
|
|
301
|
+
|
|
302
|
+
<details open>
|
|
303
|
+
<summary><strong>Click to expand full example</strong></summary>
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import {
|
|
307
|
+
SEOHead, JsonLd,
|
|
308
|
+
createSEOConfig, mergeSEOConfig, buildCanonicalUrl,
|
|
309
|
+
createProductSchema, createBreadcrumbSchema,
|
|
310
|
+
} from "react-ssr-seo-toolkit";
|
|
311
|
+
|
|
312
|
+
const siteConfig = createSEOConfig({
|
|
313
|
+
titleTemplate: "%s | Acme Store",
|
|
314
|
+
openGraph: { siteName: "Acme Store", type: "website" },
|
|
315
|
+
twitter: { card: "summary_large_image", site: "@acmestore" },
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
function ProductPage() {
|
|
319
|
+
const product = {
|
|
320
|
+
name: "Ergonomic Mechanical Keyboard",
|
|
321
|
+
description: "Premium split keyboard with Cherry MX Brown switches.",
|
|
322
|
+
price: 189.99,
|
|
323
|
+
image: "https://acmestore.com/images/keyboard.jpg",
|
|
324
|
+
brand: "Acme Peripherals",
|
|
325
|
+
sku: "ACME-KB-001",
|
|
326
|
+
inStock: true,
|
|
327
|
+
rating: 4.7,
|
|
328
|
+
reviewCount: 342,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const url = buildCanonicalUrl("https://acmestore.com", "/products/ergonomic-keyboard");
|
|
332
|
+
|
|
333
|
+
// ── Page SEO ──────────────────────────────────────────────
|
|
334
|
+
const seo = mergeSEOConfig(siteConfig, {
|
|
335
|
+
title: product.name,
|
|
336
|
+
description: product.description,
|
|
337
|
+
canonical: url,
|
|
338
|
+
openGraph: {
|
|
339
|
+
title: product.name,
|
|
340
|
+
description: product.description,
|
|
341
|
+
type: "product",
|
|
342
|
+
url,
|
|
343
|
+
images: [{ url: product.image, width: 800, height: 800, alt: product.name }],
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ── Structured Data ───────────────────────────────────────
|
|
348
|
+
const schema = createProductSchema({
|
|
349
|
+
name: product.name,
|
|
350
|
+
url,
|
|
351
|
+
description: product.description,
|
|
352
|
+
price: product.price,
|
|
353
|
+
priceCurrency: "USD",
|
|
354
|
+
availability: product.inStock ? "InStock" : "OutOfStock",
|
|
355
|
+
brand: product.brand,
|
|
356
|
+
sku: product.sku,
|
|
357
|
+
images: [product.image],
|
|
358
|
+
ratingValue: product.rating,
|
|
359
|
+
reviewCount: product.reviewCount,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const breadcrumbs = createBreadcrumbSchema([
|
|
363
|
+
{ name: "Home", url: "https://acmestore.com" },
|
|
364
|
+
{ name: "Products", url: "https://acmestore.com/products" },
|
|
365
|
+
{ name: product.name, url },
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<html>
|
|
370
|
+
<head>
|
|
371
|
+
<SEOHead {...seo} />
|
|
372
|
+
<JsonLd data={schema} />
|
|
373
|
+
<JsonLd data={breadcrumbs} />
|
|
374
|
+
</head>
|
|
375
|
+
<body>
|
|
376
|
+
<h1>{product.name}</h1>
|
|
377
|
+
<p>{product.description}</p>
|
|
378
|
+
<p>${product.price} — {product.inStock ? "In Stock" : "Out of Stock"}</p>
|
|
379
|
+
</body>
|
|
380
|
+
</html>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
</details>
|
|
386
|
+
|
|
387
|
+
<br />
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
<br />
|
|
392
|
+
|
|
393
|
+
### FAQ Page
|
|
394
|
+
|
|
395
|
+
<details open>
|
|
396
|
+
<summary><strong>Click to expand full example</strong></summary>
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
import {
|
|
400
|
+
SEOHead, JsonLd,
|
|
401
|
+
mergeSEOConfig, buildCanonicalUrl, createFAQSchema,
|
|
402
|
+
} from "react-ssr-seo-toolkit";
|
|
403
|
+
|
|
404
|
+
function FAQPage() {
|
|
405
|
+
const faqs = [
|
|
406
|
+
{ question: "What payment methods do you accept?", answer: "Visa, MasterCard, PayPal, Apple Pay." },
|
|
407
|
+
{ question: "How long does shipping take?", answer: "Standard: 3-5 business days." },
|
|
408
|
+
{ question: "What is your return policy?", answer: "30-day money-back guarantee." },
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
const seo = mergeSEOConfig(siteConfig, {
|
|
412
|
+
title: "FAQ",
|
|
413
|
+
description: "Frequently asked questions about our products and services.",
|
|
414
|
+
canonical: buildCanonicalUrl("https://mysite.com", "/faq"),
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<html>
|
|
419
|
+
<head>
|
|
420
|
+
<SEOHead {...seo} />
|
|
421
|
+
<JsonLd data={createFAQSchema(faqs)} />
|
|
422
|
+
</head>
|
|
423
|
+
<body>
|
|
424
|
+
<h1>Frequently Asked Questions</h1>
|
|
425
|
+
{faqs.map((faq, i) => (
|
|
426
|
+
<details key={i}>
|
|
427
|
+
<summary>{faq.question}</summary>
|
|
428
|
+
<p>{faq.answer}</p>
|
|
429
|
+
</details>
|
|
430
|
+
))}
|
|
431
|
+
</body>
|
|
432
|
+
</html>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
</details>
|
|
438
|
+
|
|
439
|
+
<br />
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
<br />
|
|
444
|
+
|
|
445
|
+
### Homepage (Organization + Website Schema)
|
|
446
|
+
|
|
447
|
+
<details>
|
|
448
|
+
<summary><strong>Click to expand full example</strong></summary>
|
|
449
|
+
|
|
450
|
+
```tsx
|
|
451
|
+
import {
|
|
452
|
+
SEOHead, JsonLd,
|
|
453
|
+
mergeSEOConfig,
|
|
454
|
+
createOrganizationSchema, createWebsiteSchema,
|
|
455
|
+
} from "react-ssr-seo-toolkit";
|
|
456
|
+
|
|
457
|
+
function HomePage() {
|
|
458
|
+
const seo = mergeSEOConfig(siteConfig, {
|
|
459
|
+
title: "Home",
|
|
460
|
+
canonical: "https://acme.com",
|
|
461
|
+
openGraph: {
|
|
462
|
+
title: "Acme — Building the future",
|
|
463
|
+
url: "https://acme.com",
|
|
464
|
+
images: [{ url: "https://acme.com/og-home.jpg", width: 1200, height: 630, alt: "Acme" }],
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const org = createOrganizationSchema({
|
|
469
|
+
name: "Acme Inc",
|
|
470
|
+
url: "https://acme.com",
|
|
471
|
+
logo: "https://acme.com/logo.png",
|
|
472
|
+
description: "Leading provider of quality products.",
|
|
473
|
+
sameAs: [
|
|
474
|
+
"https://twitter.com/acme",
|
|
475
|
+
"https://linkedin.com/company/acme",
|
|
476
|
+
"https://facebook.com/acme",
|
|
477
|
+
],
|
|
478
|
+
contactPoint: {
|
|
479
|
+
telephone: "+1-800-555-0199",
|
|
480
|
+
contactType: "customer service",
|
|
481
|
+
email: "support@acme.com",
|
|
482
|
+
areaServed: "US",
|
|
483
|
+
availableLanguage: ["English", "Spanish"],
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const site = createWebsiteSchema({
|
|
488
|
+
name: "Acme Inc",
|
|
489
|
+
url: "https://acme.com",
|
|
490
|
+
description: "Leading provider of quality products.",
|
|
491
|
+
searchUrl: "https://acme.com/search", // enables Google sitelinks searchbox
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<html>
|
|
496
|
+
<head>
|
|
497
|
+
<SEOHead {...seo} />
|
|
498
|
+
<JsonLd data={org} />
|
|
499
|
+
<JsonLd data={site} />
|
|
500
|
+
</head>
|
|
501
|
+
<body>
|
|
502
|
+
<h1>Welcome to Acme</h1>
|
|
503
|
+
</body>
|
|
504
|
+
</html>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
</details>
|
|
510
|
+
|
|
511
|
+
<br />
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
<br />
|
|
516
|
+
|
|
517
|
+
### Multi-Language (Hreflang)
|
|
518
|
+
|
|
519
|
+
```tsx
|
|
520
|
+
const seo = mergeSEOConfig(siteConfig, {
|
|
521
|
+
title: "Products",
|
|
522
|
+
canonical: "https://mysite.com/products",
|
|
523
|
+
alternates: [
|
|
524
|
+
{ hreflang: "en", href: "https://mysite.com/en/products" },
|
|
525
|
+
{ hreflang: "es", href: "https://mysite.com/es/products" },
|
|
526
|
+
{ hreflang: "fr", href: "https://mysite.com/fr/products" },
|
|
527
|
+
{ hreflang: "de", href: "https://mysite.com/de/products" },
|
|
528
|
+
{ hreflang: "x-default", href: "https://mysite.com/products" },
|
|
529
|
+
],
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Generates:
|
|
533
|
+
// <link rel="alternate" hreflang="en" href="https://mysite.com/en/products" />
|
|
534
|
+
// <link rel="alternate" hreflang="es" href="https://mysite.com/es/products" />
|
|
535
|
+
// <link rel="alternate" hreflang="fr" href="https://mysite.com/fr/products" />
|
|
536
|
+
// ...
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
<br />
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
<br />
|
|
544
|
+
|
|
545
|
+
### No-Index Pages (Admin, Login, Drafts)
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
import { mergeSEOConfig, noIndex, noIndexNoFollow } from "react-ssr-seo-toolkit";
|
|
549
|
+
|
|
550
|
+
// Login page: don't index, but follow links
|
|
551
|
+
const loginSeo = mergeSEOConfig(siteConfig, {
|
|
552
|
+
title: "Login",
|
|
553
|
+
robots: noIndex(), // "noindex, follow"
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Admin page: don't index, don't follow
|
|
557
|
+
const adminSeo = mergeSEOConfig(siteConfig, {
|
|
558
|
+
title: "Admin Dashboard",
|
|
559
|
+
robots: noIndexNoFollow(), // "noindex, nofollow"
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Fine-grained control
|
|
563
|
+
const archiveSeo = mergeSEOConfig(siteConfig, {
|
|
564
|
+
title: "Archive",
|
|
565
|
+
robots: {
|
|
566
|
+
index: true,
|
|
567
|
+
follow: true,
|
|
568
|
+
noarchive: true,
|
|
569
|
+
nosnippet: true,
|
|
570
|
+
maxSnippet: 50,
|
|
571
|
+
maxImagePreview: "standard",
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
<br />
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
<br />
|
|
581
|
+
|
|
582
|
+
### Combine Multiple Schemas
|
|
583
|
+
|
|
584
|
+
```tsx
|
|
585
|
+
import { composeSchemas, createOrganizationSchema, createWebsiteSchema, JsonLd } from "react-ssr-seo-toolkit";
|
|
586
|
+
|
|
587
|
+
// Merge into a single JSON-LD block with @graph array
|
|
588
|
+
const combined = composeSchemas(
|
|
589
|
+
createOrganizationSchema({ name: "Acme", url: "https://acme.com" }),
|
|
590
|
+
createWebsiteSchema({ name: "Acme", url: "https://acme.com" }),
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
<JsonLd data={combined} />
|
|
594
|
+
|
|
595
|
+
// Output: single <script> tag with {"@context":"https://schema.org","@graph":[...]}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
<br />
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
<br />
|
|
603
|
+
|
|
604
|
+
## Framework Integration
|
|
605
|
+
|
|
606
|
+
### Next.js App Router
|
|
607
|
+
|
|
608
|
+
<details open>
|
|
609
|
+
<summary><strong>Using with <code>generateMetadata()</code></strong></summary>
|
|
610
|
+
|
|
611
|
+
```tsx
|
|
612
|
+
// app/blog/[slug]/page.tsx
|
|
613
|
+
import {
|
|
614
|
+
buildTitle, buildDescription, buildCanonicalUrl,
|
|
615
|
+
createArticleSchema, safeJsonLdSerialize,
|
|
616
|
+
} from "react-ssr-seo-toolkit";
|
|
617
|
+
|
|
618
|
+
export async function generateMetadata({ params }) {
|
|
619
|
+
const post = await getPost(params.slug);
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
title: buildTitle(post.title, "%s | My Blog"),
|
|
623
|
+
description: buildDescription(post.excerpt, 160),
|
|
624
|
+
alternates: {
|
|
625
|
+
canonical: buildCanonicalUrl("https://myblog.com", `/blog/${params.slug}`),
|
|
626
|
+
},
|
|
627
|
+
openGraph: {
|
|
628
|
+
title: post.title,
|
|
629
|
+
type: "article",
|
|
630
|
+
images: [{ url: post.image, width: 1200, height: 630 }],
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export default function BlogPost({ params }) {
|
|
636
|
+
const post = getPost(params.slug);
|
|
637
|
+
|
|
638
|
+
const schema = createArticleSchema({
|
|
639
|
+
headline: post.title,
|
|
640
|
+
url: `https://myblog.com/blog/${params.slug}`,
|
|
641
|
+
datePublished: post.date,
|
|
642
|
+
author: { name: post.author },
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
return (
|
|
646
|
+
<>
|
|
647
|
+
<script
|
|
648
|
+
type="application/ld+json"
|
|
649
|
+
dangerouslySetInnerHTML={{ __html: safeJsonLdSerialize(schema) }}
|
|
650
|
+
/>
|
|
651
|
+
<article>
|
|
652
|
+
<h1>{post.title}</h1>
|
|
653
|
+
<p>{post.content}</p>
|
|
654
|
+
</article>
|
|
655
|
+
</>
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
</details>
|
|
661
|
+
|
|
662
|
+
<br />
|
|
663
|
+
|
|
664
|
+
### Next.js Pages Router
|
|
665
|
+
|
|
666
|
+
<details>
|
|
667
|
+
<summary><strong>Using with <code>next/head</code></strong></summary>
|
|
668
|
+
|
|
669
|
+
```tsx
|
|
670
|
+
// pages/about.tsx
|
|
671
|
+
import Head from "next/head";
|
|
672
|
+
import { SEOHead, mergeSEOConfig } from "react-ssr-seo-toolkit";
|
|
673
|
+
|
|
674
|
+
export default function AboutPage() {
|
|
675
|
+
const seo = mergeSEOConfig(siteConfig, {
|
|
676
|
+
title: "About Us",
|
|
677
|
+
description: "Learn about our mission.",
|
|
678
|
+
canonical: "https://mysite.com/about",
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
return (
|
|
682
|
+
<>
|
|
683
|
+
<Head>
|
|
684
|
+
<SEOHead {...seo} />
|
|
685
|
+
</Head>
|
|
686
|
+
<main>
|
|
687
|
+
<h1>About Us</h1>
|
|
688
|
+
</main>
|
|
689
|
+
</>
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
</details>
|
|
695
|
+
|
|
696
|
+
<br />
|
|
697
|
+
|
|
698
|
+
### React Router 7 SSR
|
|
699
|
+
|
|
700
|
+
<details>
|
|
701
|
+
<summary><strong>Using in root document</strong></summary>
|
|
702
|
+
|
|
703
|
+
```tsx
|
|
704
|
+
// app/root.tsx
|
|
705
|
+
import { SEOHead, JsonLd, createSEOConfig, mergeSEOConfig, createOrganizationSchema } from "react-ssr-seo-toolkit";
|
|
706
|
+
|
|
707
|
+
const siteConfig = createSEOConfig({
|
|
708
|
+
titleTemplate: "%s — Acme",
|
|
709
|
+
openGraph: { siteName: "Acme", type: "website", locale: "en_US" },
|
|
710
|
+
twitter: { card: "summary_large_image", site: "@acme" },
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
export function HomePage() {
|
|
714
|
+
const seo = mergeSEOConfig(siteConfig, {
|
|
715
|
+
title: "Home",
|
|
716
|
+
canonical: "https://acme.com",
|
|
717
|
+
jsonLd: createOrganizationSchema({
|
|
718
|
+
name: "Acme",
|
|
719
|
+
url: "https://acme.com",
|
|
720
|
+
logo: "https://acme.com/logo.png",
|
|
721
|
+
}),
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
return (
|
|
725
|
+
<html lang="en">
|
|
726
|
+
<head>
|
|
727
|
+
<meta charSet="utf-8" />
|
|
728
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
729
|
+
<SEOHead {...seo} />
|
|
730
|
+
</head>
|
|
731
|
+
<body>
|
|
732
|
+
<h1>Welcome to Acme</h1>
|
|
733
|
+
</body>
|
|
734
|
+
</html>
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
</details>
|
|
740
|
+
|
|
741
|
+
<br />
|
|
742
|
+
|
|
743
|
+
### Express + React SSR
|
|
744
|
+
|
|
745
|
+
<details>
|
|
746
|
+
<summary><strong>Using with <code>renderToString()</code></strong></summary>
|
|
747
|
+
|
|
748
|
+
```tsx
|
|
749
|
+
import express from "express";
|
|
750
|
+
import { renderToString } from "react-dom/server";
|
|
751
|
+
import { SEOHead, JsonLd, createSEOConfig, mergeSEOConfig, createProductSchema } from "react-ssr-seo-toolkit";
|
|
752
|
+
|
|
753
|
+
const app = express();
|
|
754
|
+
|
|
755
|
+
const siteConfig = createSEOConfig({
|
|
756
|
+
titleTemplate: "%s | My Store",
|
|
757
|
+
openGraph: { siteName: "My Store" },
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
function ProductPage({ product }) {
|
|
761
|
+
const seo = mergeSEOConfig(siteConfig, {
|
|
762
|
+
title: product.name,
|
|
763
|
+
description: product.description,
|
|
764
|
+
canonical: product.url,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
return (
|
|
768
|
+
<html>
|
|
769
|
+
<head>
|
|
770
|
+
<SEOHead {...seo} />
|
|
771
|
+
<JsonLd data={createProductSchema({
|
|
772
|
+
name: product.name,
|
|
773
|
+
url: product.url,
|
|
774
|
+
price: product.price,
|
|
775
|
+
})} />
|
|
776
|
+
</head>
|
|
777
|
+
<body>
|
|
778
|
+
<h1>{product.name}</h1>
|
|
779
|
+
<p>${product.price}</p>
|
|
780
|
+
</body>
|
|
781
|
+
</html>
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
app.get("/products/:id", (req, res) => {
|
|
786
|
+
const product = getProduct(req.params.id);
|
|
787
|
+
const html = renderToString(<ProductPage product={product} />);
|
|
788
|
+
res.send(`<!DOCTYPE html>${html}`);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
app.listen(3000);
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
</details>
|
|
795
|
+
|
|
796
|
+
<br />
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
<br />
|
|
801
|
+
|
|
802
|
+
## API Reference
|
|
803
|
+
|
|
804
|
+
### Config Builders
|
|
805
|
+
|
|
806
|
+
<table>
|
|
807
|
+
<tr>
|
|
808
|
+
<th>Function</th>
|
|
809
|
+
<th>What It Does</th>
|
|
810
|
+
</tr>
|
|
811
|
+
<tr>
|
|
812
|
+
<td><code>createSEOConfig(config?)</code></td>
|
|
813
|
+
<td>Create a normalized SEO config. Use for site-wide defaults.</td>
|
|
814
|
+
</tr>
|
|
815
|
+
<tr>
|
|
816
|
+
<td><code>mergeSEOConfig(base, override)</code></td>
|
|
817
|
+
<td>Deep-merge site config with page-level overrides. Arrays are replaced, not concatenated.</td>
|
|
818
|
+
</tr>
|
|
819
|
+
<tr>
|
|
820
|
+
<td><code>normalizeSEOConfig(config)</code></td>
|
|
821
|
+
<td>Trim strings, normalize URLs, clean up a config object.</td>
|
|
822
|
+
</tr>
|
|
823
|
+
</table>
|
|
824
|
+
|
|
825
|
+
<br />
|
|
826
|
+
|
|
827
|
+
### Metadata Helpers
|
|
828
|
+
|
|
829
|
+
<table>
|
|
830
|
+
<tr>
|
|
831
|
+
<th>Function</th>
|
|
832
|
+
<th>Example</th>
|
|
833
|
+
<th>Result</th>
|
|
834
|
+
</tr>
|
|
835
|
+
<tr>
|
|
836
|
+
<td><code>buildTitle(title, template)</code></td>
|
|
837
|
+
<td><code>buildTitle("About", "%s | MySite")</code></td>
|
|
838
|
+
<td><code>"About | MySite"</code></td>
|
|
839
|
+
</tr>
|
|
840
|
+
<tr>
|
|
841
|
+
<td><code>buildDescription(desc, maxLen)</code></td>
|
|
842
|
+
<td><code>buildDescription("Long text...", 160)</code></td>
|
|
843
|
+
<td>Truncated at 160 chars with ellipsis</td>
|
|
844
|
+
</tr>
|
|
845
|
+
<tr>
|
|
846
|
+
<td><code>buildCanonicalUrl(base, path)</code></td>
|
|
847
|
+
<td><code>buildCanonicalUrl("https://x.com", "/about")</code></td>
|
|
848
|
+
<td><code>"https://x.com/about"</code></td>
|
|
849
|
+
</tr>
|
|
850
|
+
<tr>
|
|
851
|
+
<td><code>buildRobotsDirectives(config)</code></td>
|
|
852
|
+
<td><code>buildRobotsDirectives({ index: false, follow: true })</code></td>
|
|
853
|
+
<td><code>"noindex, follow"</code></td>
|
|
854
|
+
</tr>
|
|
855
|
+
<tr>
|
|
856
|
+
<td><code>noIndex()</code></td>
|
|
857
|
+
<td><code>noIndex()</code></td>
|
|
858
|
+
<td><code>{ index: false, follow: true }</code></td>
|
|
859
|
+
</tr>
|
|
860
|
+
<tr>
|
|
861
|
+
<td><code>noIndexNoFollow()</code></td>
|
|
862
|
+
<td><code>noIndexNoFollow()</code></td>
|
|
863
|
+
<td><code>{ index: false, follow: false }</code></td>
|
|
864
|
+
</tr>
|
|
865
|
+
<tr>
|
|
866
|
+
<td><code>buildOpenGraph(config)</code></td>
|
|
867
|
+
<td><code>buildOpenGraph({ title: "Hi" })</code></td>
|
|
868
|
+
<td><code>[{ property: "og:title", content: "Hi" }]</code></td>
|
|
869
|
+
</tr>
|
|
870
|
+
<tr>
|
|
871
|
+
<td><code>buildTwitterMetadata(config)</code></td>
|
|
872
|
+
<td><code>buildTwitterMetadata({ card: "summary" })</code></td>
|
|
873
|
+
<td><code>[{ name: "twitter:card", content: "summary" }]</code></td>
|
|
874
|
+
</tr>
|
|
875
|
+
<tr>
|
|
876
|
+
<td><code>buildAlternateLinks(alternates)</code></td>
|
|
877
|
+
<td><code>buildAlternateLinks([{ hreflang: "en", href: "..." }])</code></td>
|
|
878
|
+
<td><code>[{ rel: "alternate", hreflang: "en", href: "..." }]</code></td>
|
|
879
|
+
</tr>
|
|
880
|
+
</table>
|
|
881
|
+
|
|
882
|
+
<br />
|
|
883
|
+
|
|
884
|
+
### JSON-LD Schema Generators
|
|
885
|
+
|
|
886
|
+
> All return a plain object with `@context: "https://schema.org"` and `@type` set.
|
|
887
|
+
|
|
888
|
+
<table>
|
|
889
|
+
<tr>
|
|
890
|
+
<th>Function</th>
|
|
891
|
+
<th>Schema Type</th>
|
|
892
|
+
<th>Use Case</th>
|
|
893
|
+
</tr>
|
|
894
|
+
<tr>
|
|
895
|
+
<td><code>createOrganizationSchema(input)</code></td>
|
|
896
|
+
<td>Organization</td>
|
|
897
|
+
<td>Company info, logo, social links, contact</td>
|
|
898
|
+
</tr>
|
|
899
|
+
<tr>
|
|
900
|
+
<td><code>createWebsiteSchema(input)</code></td>
|
|
901
|
+
<td>WebSite</td>
|
|
902
|
+
<td>Site name, sitelinks searchbox</td>
|
|
903
|
+
</tr>
|
|
904
|
+
<tr>
|
|
905
|
+
<td><code>createArticleSchema(input)</code></td>
|
|
906
|
+
<td>Article</td>
|
|
907
|
+
<td>Blog posts, news articles, authors, dates</td>
|
|
908
|
+
</tr>
|
|
909
|
+
<tr>
|
|
910
|
+
<td><code>createProductSchema(input)</code></td>
|
|
911
|
+
<td>Product</td>
|
|
912
|
+
<td>E-commerce: price, brand, SKU, ratings, availability</td>
|
|
913
|
+
</tr>
|
|
914
|
+
<tr>
|
|
915
|
+
<td><code>createBreadcrumbSchema(items)</code></td>
|
|
916
|
+
<td>BreadcrumbList</td>
|
|
917
|
+
<td>Navigation hierarchy</td>
|
|
918
|
+
</tr>
|
|
919
|
+
<tr>
|
|
920
|
+
<td><code>createFAQSchema(items)</code></td>
|
|
921
|
+
<td>FAQPage</td>
|
|
922
|
+
<td>FAQ pages with question + answer pairs</td>
|
|
923
|
+
</tr>
|
|
924
|
+
<tr>
|
|
925
|
+
<td><code>composeSchemas(...schemas)</code></td>
|
|
926
|
+
<td>@graph</td>
|
|
927
|
+
<td>Combine multiple schemas into one JSON-LD block</td>
|
|
928
|
+
</tr>
|
|
929
|
+
</table>
|
|
930
|
+
|
|
931
|
+
<br />
|
|
932
|
+
|
|
933
|
+
### Utilities
|
|
934
|
+
|
|
935
|
+
<table>
|
|
936
|
+
<tr>
|
|
937
|
+
<th>Function</th>
|
|
938
|
+
<th>What It Does</th>
|
|
939
|
+
</tr>
|
|
940
|
+
<tr>
|
|
941
|
+
<td><code>safeJsonLdSerialize(data)</code></td>
|
|
942
|
+
<td>Serialize JSON-LD safely for <code><script></code> tags — escapes <code><</code>, <code>></code>, <code>&</code> to prevent XSS</td>
|
|
943
|
+
</tr>
|
|
944
|
+
<tr>
|
|
945
|
+
<td><code>normalizeUrl(url)</code></td>
|
|
946
|
+
<td>Trim whitespace, remove trailing slashes</td>
|
|
947
|
+
</tr>
|
|
948
|
+
<tr>
|
|
949
|
+
<td><code>buildFullUrl(base, path?)</code></td>
|
|
950
|
+
<td>Combine base URL with path</td>
|
|
951
|
+
</tr>
|
|
952
|
+
<tr>
|
|
953
|
+
<td><code>omitEmpty(obj)</code></td>
|
|
954
|
+
<td>Remove keys with <code>undefined</code>, <code>null</code>, or empty string values</td>
|
|
955
|
+
</tr>
|
|
956
|
+
<tr>
|
|
957
|
+
<td><code>deepMerge(base, override)</code></td>
|
|
958
|
+
<td>Deep-merge two objects (arrays replaced, not concatenated)</td>
|
|
959
|
+
</tr>
|
|
960
|
+
</table>
|
|
961
|
+
|
|
962
|
+
<br />
|
|
963
|
+
|
|
964
|
+
### React Components
|
|
965
|
+
|
|
966
|
+
#### `<SEOHead>`
|
|
967
|
+
|
|
968
|
+
Renders all SEO tags as React elements. Place inside `<head>`.
|
|
969
|
+
|
|
970
|
+
```tsx
|
|
971
|
+
<SEOHead
|
|
972
|
+
title="My Page"
|
|
973
|
+
titleTemplate="%s | MySite"
|
|
974
|
+
description="Page description here."
|
|
975
|
+
canonical="https://mysite.com/page"
|
|
976
|
+
robots={{ index: true, follow: true }}
|
|
977
|
+
openGraph={{
|
|
978
|
+
title: "My Page",
|
|
979
|
+
description: "For social sharing.",
|
|
980
|
+
type: "website",
|
|
981
|
+
url: "https://mysite.com/page",
|
|
982
|
+
siteName: "MySite",
|
|
983
|
+
locale: "en_US",
|
|
984
|
+
images: [{ url: "https://mysite.com/og.jpg", width: 1200, height: 630, alt: "Preview" }],
|
|
985
|
+
}}
|
|
986
|
+
twitter={{
|
|
987
|
+
card: "summary_large_image",
|
|
988
|
+
site: "@mysite",
|
|
989
|
+
creator: "@author",
|
|
990
|
+
title: "My Page",
|
|
991
|
+
image: "https://mysite.com/twitter.jpg",
|
|
992
|
+
}}
|
|
993
|
+
alternates={[
|
|
994
|
+
{ hreflang: "en", href: "https://mysite.com/en/page" },
|
|
995
|
+
{ hreflang: "es", href: "https://mysite.com/es/page" },
|
|
996
|
+
]}
|
|
997
|
+
additionalMetaTags={[
|
|
998
|
+
{ name: "author", content: "Jane Doe" },
|
|
999
|
+
{ property: "article:published_time", content: "2025-06-15" },
|
|
1000
|
+
]}
|
|
1001
|
+
additionalLinkTags={[
|
|
1002
|
+
{ rel: "icon", href: "/favicon.ico" },
|
|
1003
|
+
{ rel: "apple-touch-icon", href: "/apple-touch-icon.png", sizes: "180x180" },
|
|
1004
|
+
]}
|
|
1005
|
+
jsonLd={createArticleSchema({ headline: "...", url: "..." })}
|
|
1006
|
+
/>
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
#### `<JsonLd>`
|
|
1010
|
+
|
|
1011
|
+
Standalone JSON-LD `<script>` tag renderer.
|
|
1012
|
+
|
|
1013
|
+
```tsx
|
|
1014
|
+
<JsonLd data={createProductSchema({ name: "Widget", url: "...", price: 29.99 })} />
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
<br />
|
|
1018
|
+
|
|
1019
|
+
### TypeScript Types
|
|
1020
|
+
|
|
1021
|
+
```tsx
|
|
1022
|
+
import type {
|
|
1023
|
+
SEOConfig,
|
|
1024
|
+
OpenGraphConfig,
|
|
1025
|
+
OpenGraphImage,
|
|
1026
|
+
OpenGraphType, // "website" | "article" | "product" | "profile" | ...
|
|
1027
|
+
TwitterConfig,
|
|
1028
|
+
TwitterCardType, // "summary" | "summary_large_image" | "app" | "player"
|
|
1029
|
+
RobotsConfig,
|
|
1030
|
+
AlternateLink,
|
|
1031
|
+
JSONLDBase,
|
|
1032
|
+
BreadcrumbItem,
|
|
1033
|
+
OrganizationSchemaInput,
|
|
1034
|
+
WebsiteSchemaInput,
|
|
1035
|
+
ArticleSchemaInput,
|
|
1036
|
+
ProductSchemaInput,
|
|
1037
|
+
FAQItem,
|
|
1038
|
+
SEOHeadProps,
|
|
1039
|
+
JsonLdProps,
|
|
1040
|
+
} from "react-ssr-seo-toolkit";
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
<br />
|
|
1044
|
+
|
|
1045
|
+
---
|
|
1046
|
+
|
|
1047
|
+
<br />
|
|
1048
|
+
|
|
1049
|
+
## Live Demo
|
|
1050
|
+
|
|
1051
|
+
Try it locally — the repo includes a **working Express SSR demo** with 5 pages:
|
|
1052
|
+
|
|
1053
|
+
```bash
|
|
1054
|
+
git clone https://github.com/Tonmoy01/react-ssr-seo.git
|
|
1055
|
+
cd react-ssr-seo
|
|
1056
|
+
npm install
|
|
1057
|
+
npm run demo
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
Then open your browser:
|
|
1061
|
+
|
|
1062
|
+
<table>
|
|
1063
|
+
<tr>
|
|
1064
|
+
<th>URL</th>
|
|
1065
|
+
<th>Page</th>
|
|
1066
|
+
<th>SEO Features Demonstrated</th>
|
|
1067
|
+
</tr>
|
|
1068
|
+
<tr>
|
|
1069
|
+
<td><code>localhost:3000</code></td>
|
|
1070
|
+
<td>Home</td>
|
|
1071
|
+
<td>Organization + Website schema, hreflang, OG images</td>
|
|
1072
|
+
</tr>
|
|
1073
|
+
<tr>
|
|
1074
|
+
<td><code>localhost:3000/article</code></td>
|
|
1075
|
+
<td>Article</td>
|
|
1076
|
+
<td>Article schema, breadcrumbs, multiple authors, Twitter cards</td>
|
|
1077
|
+
</tr>
|
|
1078
|
+
<tr>
|
|
1079
|
+
<td><code>localhost:3000/product</code></td>
|
|
1080
|
+
<td>Product</td>
|
|
1081
|
+
<td>Product schema, pricing, ratings, availability, breadcrumbs</td>
|
|
1082
|
+
</tr>
|
|
1083
|
+
<tr>
|
|
1084
|
+
<td><code>localhost:3000/faq</code></td>
|
|
1085
|
+
<td>FAQ</td>
|
|
1086
|
+
<td>FAQPage schema with Q&A pairs</td>
|
|
1087
|
+
</tr>
|
|
1088
|
+
<tr>
|
|
1089
|
+
<td><code>localhost:3000/noindex</code></td>
|
|
1090
|
+
<td>No-Index</td>
|
|
1091
|
+
<td>Robots noindex directive</td>
|
|
1092
|
+
</tr>
|
|
1093
|
+
</table>
|
|
1094
|
+
|
|
1095
|
+
> **Tip:** Right-click any page and **View Page Source** to see all SEO tags in the raw HTML.
|
|
1096
|
+
|
|
1097
|
+
<br />
|
|
1098
|
+
|
|
1099
|
+
---
|
|
1100
|
+
|
|
1101
|
+
<br />
|
|
1102
|
+
|
|
1103
|
+
## Development
|
|
1104
|
+
|
|
1105
|
+
```bash
|
|
1106
|
+
npm install # install dependencies
|
|
1107
|
+
npm run build # build the library
|
|
1108
|
+
npm run dev # watch mode (auto-rebuild)
|
|
1109
|
+
npm test # run tests
|
|
1110
|
+
npm run test:watch # tests in watch mode
|
|
1111
|
+
npm run lint # type check
|
|
1112
|
+
npm run clean # clean build output
|
|
1113
|
+
npm run demo # run demo server
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
<br />
|
|
1117
|
+
|
|
1118
|
+
---
|
|
1119
|
+
|
|
1120
|
+
<br />
|
|
1121
|
+
|
|
1122
|
+
## Troubleshooting
|
|
1123
|
+
|
|
1124
|
+
<details>
|
|
1125
|
+
<summary><strong>"Cannot find module 'react-ssr-seo'"</strong></summary>
|
|
1126
|
+
|
|
1127
|
+
<br />
|
|
1128
|
+
|
|
1129
|
+
Ensure the package is installed and your bundler supports the `exports` field in `package.json`. If using an older bundler, try importing from `react-ssr-seo/dist/index.js` directly.
|
|
1130
|
+
</details>
|
|
1131
|
+
|
|
1132
|
+
<details>
|
|
1133
|
+
<summary><strong>Hydration mismatch warnings</strong></summary>
|
|
1134
|
+
|
|
1135
|
+
<br />
|
|
1136
|
+
|
|
1137
|
+
`<SEOHead>` produces deterministic output. If you see hydration warnings, ensure the same config object is used on both server and client. Avoid using `Date.now()` or random values in your SEO config.
|
|
1138
|
+
</details>
|
|
1139
|
+
|
|
1140
|
+
<details>
|
|
1141
|
+
<summary><strong>JSON-LD not appearing in page source</strong></summary>
|
|
1142
|
+
|
|
1143
|
+
<br />
|
|
1144
|
+
|
|
1145
|
+
Make sure `<JsonLd>` or `<script type="application/ld+json">` is inside `<head>` and rendered during SSR — not in a client-only `useEffect`.
|
|
1146
|
+
</details>
|
|
1147
|
+
|
|
1148
|
+
<details>
|
|
1149
|
+
<summary><strong>TypeScript errors</strong></summary>
|
|
1150
|
+
|
|
1151
|
+
<br />
|
|
1152
|
+
|
|
1153
|
+
All types are exported. Import them directly:
|
|
1154
|
+
|
|
1155
|
+
```tsx
|
|
1156
|
+
import type { SEOConfig, OpenGraphConfig } from "react-ssr-seo-toolkit";
|
|
1157
|
+
```
|
|
1158
|
+
</details>
|
|
1159
|
+
|
|
1160
|
+
<br />
|
|
1161
|
+
|
|
1162
|
+
---
|
|
1163
|
+
|
|
1164
|
+
<br />
|
|
1165
|
+
|
|
1166
|
+
## Contributing
|
|
1167
|
+
|
|
1168
|
+
1. Fork the repo
|
|
1169
|
+
2. Create a feature branch — `git checkout -b feature/my-feature`
|
|
1170
|
+
3. Make your changes with tests
|
|
1171
|
+
4. Run `npm test && npm run lint`
|
|
1172
|
+
5. Open a PR
|
|
1173
|
+
|
|
1174
|
+
<br />
|
|
1175
|
+
|
|
1176
|
+
---
|
|
1177
|
+
|
|
1178
|
+
<div align="center">
|
|
1179
|
+
|
|
1180
|
+
<br />
|
|
1181
|
+
|
|
1182
|
+
**MIT License** • Made by [Tonmoy](https://github.com/Tonmoy01)
|
|
1183
|
+
|
|
1184
|
+
<br />
|
|
1185
|
+
|
|
1186
|
+
</div>
|