react-ssr-seo-toolkit 1.0.2 → 1.0.3
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 +117 -324
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
<br />
|
|
4
4
|
|
|
5
|
-
<img src="https://img.shields.io/badge/react--ssr--seo-v1.0.
|
|
5
|
+
<img src="https://img.shields.io/badge/react--ssr--seo--toolkit-v1.0.3-000000?style=for-the-badge&labelColor=000000" alt="react-ssr-seo-toolkit" />
|
|
6
6
|
|
|
7
7
|
<br />
|
|
8
8
|
<br />
|
|
9
9
|
|
|
10
|
-
# `react-ssr-seo`
|
|
10
|
+
# `react-ssr-seo-toolkit`
|
|
11
11
|
|
|
12
12
|
### The Complete SEO Toolkit for React SSR Applications
|
|
13
13
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
[](./LICENSE)
|
|
21
21
|
|
|
22
|
-
[](https://bundlephobia.com/package/react-ssr-seo-toolkit)
|
|
22
|
+
[](https://bundlephobia.com/package/react-ssr-seo-toolkit)
|
|
23
23
|
|
|
24
24
|
<br />
|
|
25
25
|
|
|
@@ -98,46 +98,72 @@ All in one package. Zero dependencies. Fully typed. SSR-safe.
|
|
|
98
98
|
npm install react-ssr-seo-toolkit
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
> **Requires:** `react >= 18.0.0` as a peer dependency. Zero other dependencies.
|
|
102
|
+
|
|
103
|
+
<br />
|
|
104
|
+
|
|
105
|
+
### 2. Project Structure
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
You only need **two new files** — a shared SEO config and your page components:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
my-app/
|
|
111
|
+
├── config/
|
|
112
|
+
│ └── seo.ts ← shared SEO defaults (Step 3)
|
|
113
|
+
├── pages/
|
|
114
|
+
│ ├── HomePage.tsx ← each page merges its own SEO
|
|
115
|
+
│ ├── AboutPage.tsx
|
|
116
|
+
│ └── BlogPost.tsx
|
|
117
|
+
├── components/
|
|
118
|
+
│ └── Layout.tsx ← wraps <SEOHead> + <JsonLd>
|
|
119
|
+
├── server.tsx ← Express / SSR entry point
|
|
120
|
+
└── package.json
|
|
121
|
+
```
|
|
107
122
|
|
|
108
123
|
<br />
|
|
109
124
|
|
|
110
|
-
###
|
|
125
|
+
### 3. Create Site Config (once)
|
|
126
|
+
|
|
127
|
+
This file holds defaults that every page inherits. Pages override only what they need.
|
|
111
128
|
|
|
112
129
|
```tsx
|
|
130
|
+
// config/seo.ts
|
|
113
131
|
import { createSEOConfig } from "react-ssr-seo-toolkit";
|
|
114
132
|
|
|
115
|
-
const siteConfig = createSEOConfig({
|
|
133
|
+
export const siteConfig = createSEOConfig({
|
|
116
134
|
titleTemplate: "%s | MySite", // auto-appends " | MySite" to every page title
|
|
117
|
-
|
|
135
|
+
description: "Default site description for SEO.",
|
|
136
|
+
openGraph: { siteName: "MySite", type: "website", locale: "en_US" },
|
|
118
137
|
twitter: { card: "summary_large_image", site: "@mysite" },
|
|
119
138
|
});
|
|
139
|
+
|
|
140
|
+
export const SITE_URL = "https://mysite.com";
|
|
120
141
|
```
|
|
121
142
|
|
|
143
|
+
> **Tip:** `titleTemplate` uses `%s` as a placeholder. Setting `title: "About"` renders as `About | MySite`.
|
|
144
|
+
|
|
122
145
|
<br />
|
|
123
146
|
|
|
124
|
-
###
|
|
147
|
+
### 4. Add to Any Page
|
|
148
|
+
|
|
149
|
+
Merge the shared config with page-specific values, then render with `<SEOHead>`.
|
|
125
150
|
|
|
126
151
|
```tsx
|
|
152
|
+
// pages/AboutPage.tsx
|
|
127
153
|
import { SEOHead, mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
|
|
154
|
+
import { siteConfig, SITE_URL } from "../config/seo";
|
|
128
155
|
|
|
129
|
-
function AboutPage() {
|
|
156
|
+
export function AboutPage() {
|
|
130
157
|
const seo = mergeSEOConfig(siteConfig, {
|
|
131
158
|
title: "About Us",
|
|
132
159
|
description: "Learn about our company and mission.",
|
|
133
|
-
canonical: buildCanonicalUrl(
|
|
160
|
+
canonical: buildCanonicalUrl(SITE_URL, "/about"),
|
|
134
161
|
});
|
|
135
162
|
|
|
136
163
|
return (
|
|
137
164
|
<html>
|
|
138
165
|
<head>
|
|
139
166
|
<SEOHead {...seo} />
|
|
140
|
-
{/* Renders: <title>, <meta>, <link>, <script type="application/ld+json"> */}
|
|
141
167
|
</head>
|
|
142
168
|
<body>
|
|
143
169
|
<h1>About Us</h1>
|
|
@@ -147,7 +173,7 @@ function AboutPage() {
|
|
|
147
173
|
}
|
|
148
174
|
```
|
|
149
175
|
|
|
150
|
-
**
|
|
176
|
+
**That's it.** You now have full SEO on every page. Keep reading for structured data and framework examples.
|
|
151
177
|
|
|
152
178
|
<br />
|
|
153
179
|
|
|
@@ -163,29 +189,21 @@ function AboutPage() {
|
|
|
163
189
|
|
|
164
190
|
### Blog / Article Page
|
|
165
191
|
|
|
166
|
-
<details open>
|
|
167
|
-
<summary><strong>Click to expand full example</strong></summary>
|
|
168
|
-
|
|
169
192
|
```tsx
|
|
193
|
+
// pages/BlogPost.tsx
|
|
170
194
|
import {
|
|
171
195
|
SEOHead, JsonLd,
|
|
172
|
-
|
|
196
|
+
mergeSEOConfig, buildCanonicalUrl,
|
|
173
197
|
createArticleSchema, createBreadcrumbSchema,
|
|
174
198
|
} from "react-ssr-seo-toolkit";
|
|
199
|
+
import { siteConfig, SITE_URL } from "../config/seo";
|
|
175
200
|
|
|
176
|
-
|
|
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() {
|
|
201
|
+
export function BlogPostPage() {
|
|
184
202
|
// ── Page SEO ──────────────────────────────────────────────
|
|
185
203
|
const seo = mergeSEOConfig(siteConfig, {
|
|
186
204
|
title: "How to Build an SSR App",
|
|
187
205
|
description: "A complete guide to building server-rendered React apps with proper SEO.",
|
|
188
|
-
canonical: buildCanonicalUrl(
|
|
206
|
+
canonical: buildCanonicalUrl(SITE_URL, "/blog/ssr-guide"),
|
|
189
207
|
openGraph: {
|
|
190
208
|
title: "How to Build an SSR App",
|
|
191
209
|
description: "A complete guide to SSR with React.",
|
|
@@ -246,10 +264,9 @@ function BlogPostPage() {
|
|
|
246
264
|
}
|
|
247
265
|
```
|
|
248
266
|
|
|
249
|
-
|
|
267
|
+
<br />
|
|
250
268
|
|
|
251
|
-
|
|
252
|
-
<summary><strong>See the HTML output this generates</strong></summary>
|
|
269
|
+
### Generated HTML Output
|
|
253
270
|
|
|
254
271
|
```html
|
|
255
272
|
<head>
|
|
@@ -257,40 +274,26 @@ function BlogPostPage() {
|
|
|
257
274
|
<title>How to Build an SSR App | My Blog</title>
|
|
258
275
|
<meta name="description" content="A complete guide to building server-rendered React apps..." />
|
|
259
276
|
<link rel="canonical" href="https://myblog.com/blog/ssr-guide" />
|
|
260
|
-
<meta name="robots" content="index, follow" />
|
|
261
277
|
|
|
262
|
-
<!-- Open Graph
|
|
278
|
+
<!-- Open Graph -->
|
|
263
279
|
<meta property="og:title" content="How to Build an SSR App" />
|
|
264
280
|
<meta property="og:description" content="A complete guide to SSR with React." />
|
|
265
281
|
<meta property="og:type" content="article" />
|
|
266
282
|
<meta property="og:url" content="https://myblog.com/blog/ssr-guide" />
|
|
267
283
|
<meta property="og:site_name" content="My Blog" />
|
|
268
284
|
<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
285
|
|
|
273
286
|
<!-- Twitter Card -->
|
|
274
287
|
<meta name="twitter:card" content="summary_large_image" />
|
|
275
288
|
<meta name="twitter:site" content="@myblog" />
|
|
276
|
-
<meta name="twitter:creator" content="@authorhandle" />
|
|
277
289
|
<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
290
|
|
|
285
|
-
<!-- JSON-LD
|
|
286
|
-
<script type="application/ld+json">
|
|
287
|
-
|
|
288
|
-
</script>
|
|
291
|
+
<!-- JSON-LD -->
|
|
292
|
+
<script type="application/ld+json">{"@context":"https://schema.org","@type":"Article",...}</script>
|
|
293
|
+
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList",...}</script>
|
|
289
294
|
</head>
|
|
290
295
|
```
|
|
291
296
|
|
|
292
|
-
</details>
|
|
293
|
-
|
|
294
297
|
<br />
|
|
295
298
|
|
|
296
299
|
---
|
|
@@ -299,21 +302,13 @@ function BlogPostPage() {
|
|
|
299
302
|
|
|
300
303
|
### E-Commerce Product Page
|
|
301
304
|
|
|
302
|
-
<details open>
|
|
303
|
-
<summary><strong>Click to expand full example</strong></summary>
|
|
304
|
-
|
|
305
305
|
```tsx
|
|
306
306
|
import {
|
|
307
307
|
SEOHead, JsonLd,
|
|
308
|
-
|
|
308
|
+
mergeSEOConfig, buildCanonicalUrl,
|
|
309
309
|
createProductSchema, createBreadcrumbSchema,
|
|
310
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
|
-
});
|
|
311
|
+
import { siteConfig, SITE_URL } from "../config/seo";
|
|
317
312
|
|
|
318
313
|
function ProductPage() {
|
|
319
314
|
const product = {
|
|
@@ -328,9 +323,8 @@ function ProductPage() {
|
|
|
328
323
|
reviewCount: 342,
|
|
329
324
|
};
|
|
330
325
|
|
|
331
|
-
const url = buildCanonicalUrl(
|
|
326
|
+
const url = buildCanonicalUrl(SITE_URL, "/products/ergonomic-keyboard");
|
|
332
327
|
|
|
333
|
-
// ── Page SEO ──────────────────────────────────────────────
|
|
334
328
|
const seo = mergeSEOConfig(siteConfig, {
|
|
335
329
|
title: product.name,
|
|
336
330
|
description: product.description,
|
|
@@ -344,7 +338,6 @@ function ProductPage() {
|
|
|
344
338
|
},
|
|
345
339
|
});
|
|
346
340
|
|
|
347
|
-
// ── Structured Data ───────────────────────────────────────
|
|
348
341
|
const schema = createProductSchema({
|
|
349
342
|
name: product.name,
|
|
350
343
|
url,
|
|
@@ -359,31 +352,21 @@ function ProductPage() {
|
|
|
359
352
|
reviewCount: product.reviewCount,
|
|
360
353
|
});
|
|
361
354
|
|
|
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
355
|
return (
|
|
369
356
|
<html>
|
|
370
357
|
<head>
|
|
371
358
|
<SEOHead {...seo} />
|
|
372
359
|
<JsonLd data={schema} />
|
|
373
|
-
<JsonLd data={breadcrumbs} />
|
|
374
360
|
</head>
|
|
375
361
|
<body>
|
|
376
362
|
<h1>{product.name}</h1>
|
|
377
|
-
<p
|
|
378
|
-
<p>${product.price} — {product.inStock ? "In Stock" : "Out of Stock"}</p>
|
|
363
|
+
<p>${product.price}</p>
|
|
379
364
|
</body>
|
|
380
365
|
</html>
|
|
381
366
|
);
|
|
382
367
|
}
|
|
383
368
|
```
|
|
384
369
|
|
|
385
|
-
</details>
|
|
386
|
-
|
|
387
370
|
<br />
|
|
388
371
|
|
|
389
372
|
---
|
|
@@ -392,14 +375,12 @@ function ProductPage() {
|
|
|
392
375
|
|
|
393
376
|
### FAQ Page
|
|
394
377
|
|
|
395
|
-
<details open>
|
|
396
|
-
<summary><strong>Click to expand full example</strong></summary>
|
|
397
|
-
|
|
398
378
|
```tsx
|
|
399
379
|
import {
|
|
400
380
|
SEOHead, JsonLd,
|
|
401
381
|
mergeSEOConfig, buildCanonicalUrl, createFAQSchema,
|
|
402
382
|
} from "react-ssr-seo-toolkit";
|
|
383
|
+
import { siteConfig, SITE_URL } from "../config/seo";
|
|
403
384
|
|
|
404
385
|
function FAQPage() {
|
|
405
386
|
const faqs = [
|
|
@@ -411,7 +392,7 @@ function FAQPage() {
|
|
|
411
392
|
const seo = mergeSEOConfig(siteConfig, {
|
|
412
393
|
title: "FAQ",
|
|
413
394
|
description: "Frequently asked questions about our products and services.",
|
|
414
|
-
canonical: buildCanonicalUrl(
|
|
395
|
+
canonical: buildCanonicalUrl(SITE_URL, "/faq"),
|
|
415
396
|
});
|
|
416
397
|
|
|
417
398
|
return (
|
|
@@ -434,8 +415,6 @@ function FAQPage() {
|
|
|
434
415
|
}
|
|
435
416
|
```
|
|
436
417
|
|
|
437
|
-
</details>
|
|
438
|
-
|
|
439
418
|
<br />
|
|
440
419
|
|
|
441
420
|
---
|
|
@@ -444,15 +423,13 @@ function FAQPage() {
|
|
|
444
423
|
|
|
445
424
|
### Homepage (Organization + Website Schema)
|
|
446
425
|
|
|
447
|
-
<details>
|
|
448
|
-
<summary><strong>Click to expand full example</strong></summary>
|
|
449
|
-
|
|
450
426
|
```tsx
|
|
451
427
|
import {
|
|
452
428
|
SEOHead, JsonLd,
|
|
453
429
|
mergeSEOConfig,
|
|
454
430
|
createOrganizationSchema, createWebsiteSchema,
|
|
455
431
|
} from "react-ssr-seo-toolkit";
|
|
432
|
+
import { siteConfig } from "../config/seo";
|
|
456
433
|
|
|
457
434
|
function HomePage() {
|
|
458
435
|
const seo = mergeSEOConfig(siteConfig, {
|
|
@@ -506,8 +483,6 @@ function HomePage() {
|
|
|
506
483
|
}
|
|
507
484
|
```
|
|
508
485
|
|
|
509
|
-
</details>
|
|
510
|
-
|
|
511
486
|
<br />
|
|
512
487
|
|
|
513
488
|
---
|
|
@@ -524,7 +499,6 @@ const seo = mergeSEOConfig(siteConfig, {
|
|
|
524
499
|
{ hreflang: "en", href: "https://mysite.com/en/products" },
|
|
525
500
|
{ hreflang: "es", href: "https://mysite.com/es/products" },
|
|
526
501
|
{ hreflang: "fr", href: "https://mysite.com/fr/products" },
|
|
527
|
-
{ hreflang: "de", href: "https://mysite.com/de/products" },
|
|
528
502
|
{ hreflang: "x-default", href: "https://mysite.com/products" },
|
|
529
503
|
],
|
|
530
504
|
});
|
|
@@ -532,7 +506,6 @@ const seo = mergeSEOConfig(siteConfig, {
|
|
|
532
506
|
// Generates:
|
|
533
507
|
// <link rel="alternate" hreflang="en" href="https://mysite.com/en/products" />
|
|
534
508
|
// <link rel="alternate" hreflang="es" href="https://mysite.com/es/products" />
|
|
535
|
-
// <link rel="alternate" hreflang="fr" href="https://mysite.com/fr/products" />
|
|
536
509
|
// ...
|
|
537
510
|
```
|
|
538
511
|
|
|
@@ -605,9 +578,6 @@ const combined = composeSchemas(
|
|
|
605
578
|
|
|
606
579
|
### Next.js App Router
|
|
607
580
|
|
|
608
|
-
<details open>
|
|
609
|
-
<summary><strong>Using with <code>generateMetadata()</code></strong></summary>
|
|
610
|
-
|
|
611
581
|
```tsx
|
|
612
582
|
// app/blog/[slug]/page.tsx
|
|
613
583
|
import {
|
|
@@ -657,15 +627,10 @@ export default function BlogPost({ params }) {
|
|
|
657
627
|
}
|
|
658
628
|
```
|
|
659
629
|
|
|
660
|
-
</details>
|
|
661
|
-
|
|
662
630
|
<br />
|
|
663
631
|
|
|
664
632
|
### Next.js Pages Router
|
|
665
633
|
|
|
666
|
-
<details>
|
|
667
|
-
<summary><strong>Using with <code>next/head</code></strong></summary>
|
|
668
|
-
|
|
669
634
|
```tsx
|
|
670
635
|
// pages/about.tsx
|
|
671
636
|
import Head from "next/head";
|
|
@@ -691,34 +656,19 @@ export default function AboutPage() {
|
|
|
691
656
|
}
|
|
692
657
|
```
|
|
693
658
|
|
|
694
|
-
</details>
|
|
695
|
-
|
|
696
659
|
<br />
|
|
697
660
|
|
|
698
661
|
### React Router 7 SSR
|
|
699
662
|
|
|
700
|
-
<details>
|
|
701
|
-
<summary><strong>Using in root document</strong></summary>
|
|
702
|
-
|
|
703
663
|
```tsx
|
|
704
664
|
// app/root.tsx
|
|
705
|
-
import { SEOHead,
|
|
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
|
-
});
|
|
665
|
+
import { SEOHead, mergeSEOConfig, createOrganizationSchema, JsonLd } from "react-ssr-seo-toolkit";
|
|
666
|
+
import { siteConfig } from "./config/seo";
|
|
712
667
|
|
|
713
668
|
export function HomePage() {
|
|
714
669
|
const seo = mergeSEOConfig(siteConfig, {
|
|
715
670
|
title: "Home",
|
|
716
671
|
canonical: "https://acme.com",
|
|
717
|
-
jsonLd: createOrganizationSchema({
|
|
718
|
-
name: "Acme",
|
|
719
|
-
url: "https://acme.com",
|
|
720
|
-
logo: "https://acme.com/logo.png",
|
|
721
|
-
}),
|
|
722
672
|
});
|
|
723
673
|
|
|
724
674
|
return (
|
|
@@ -736,27 +686,19 @@ export function HomePage() {
|
|
|
736
686
|
}
|
|
737
687
|
```
|
|
738
688
|
|
|
739
|
-
</details>
|
|
740
|
-
|
|
741
689
|
<br />
|
|
742
690
|
|
|
743
691
|
### Express + React SSR
|
|
744
692
|
|
|
745
|
-
<details>
|
|
746
|
-
<summary><strong>Using with <code>renderToString()</code></strong></summary>
|
|
747
|
-
|
|
748
693
|
```tsx
|
|
694
|
+
// server.tsx
|
|
749
695
|
import express from "express";
|
|
750
696
|
import { renderToString } from "react-dom/server";
|
|
751
|
-
import { SEOHead, JsonLd,
|
|
697
|
+
import { SEOHead, JsonLd, mergeSEOConfig, createProductSchema } from "react-ssr-seo-toolkit";
|
|
698
|
+
import { siteConfig } from "./config/seo";
|
|
752
699
|
|
|
753
700
|
const app = express();
|
|
754
701
|
|
|
755
|
-
const siteConfig = createSEOConfig({
|
|
756
|
-
titleTemplate: "%s | My Store",
|
|
757
|
-
openGraph: { siteName: "My Store" },
|
|
758
|
-
});
|
|
759
|
-
|
|
760
702
|
function ProductPage({ product }) {
|
|
761
703
|
const seo = mergeSEOConfig(siteConfig, {
|
|
762
704
|
title: product.name,
|
|
@@ -791,8 +733,6 @@ app.get("/products/:id", (req, res) => {
|
|
|
791
733
|
app.listen(3000);
|
|
792
734
|
```
|
|
793
735
|
|
|
794
|
-
</details>
|
|
795
|
-
|
|
796
736
|
<br />
|
|
797
737
|
|
|
798
738
|
---
|
|
@@ -803,161 +743,55 @@ app.listen(3000);
|
|
|
803
743
|
|
|
804
744
|
### Config Builders
|
|
805
745
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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>
|
|
746
|
+
| Function | What It Does |
|
|
747
|
+
|---|---|
|
|
748
|
+
| `createSEOConfig(config?)` | Create a normalized SEO config. Use for site-wide defaults. |
|
|
749
|
+
| `mergeSEOConfig(base, override)` | Deep-merge site config with page-level overrides. Arrays are replaced, not concatenated. |
|
|
750
|
+
| `normalizeSEOConfig(config)` | Trim strings, normalize URLs, clean up a config object. |
|
|
824
751
|
|
|
825
752
|
<br />
|
|
826
753
|
|
|
827
754
|
### Metadata Helpers
|
|
828
755
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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>
|
|
756
|
+
| Function | Example | Result |
|
|
757
|
+
|---|---|---|
|
|
758
|
+
| `buildTitle(title, template)` | `buildTitle("About", "%s \| MySite")` | `"About \| MySite"` |
|
|
759
|
+
| `buildDescription(desc, maxLen)` | `buildDescription("Long text...", 160)` | Truncated at 160 chars |
|
|
760
|
+
| `buildCanonicalUrl(base, path)` | `buildCanonicalUrl("https://x.com", "/about")` | `"https://x.com/about"` |
|
|
761
|
+
| `buildRobotsDirectives(config)` | `buildRobotsDirectives({ index: false })` | `"noindex, follow"` |
|
|
762
|
+
| `noIndex()` | `noIndex()` | `{ index: false, follow: true }` |
|
|
763
|
+
| `noIndexNoFollow()` | `noIndexNoFollow()` | `{ index: false, follow: false }` |
|
|
764
|
+
| `buildOpenGraph(config)` | `buildOpenGraph({ title: "Hi" })` | `[{ property: "og:title", content: "Hi" }]` |
|
|
765
|
+
| `buildTwitterMetadata(config)` | `buildTwitterMetadata({ card: "summary" })` | `[{ name: "twitter:card", content: "summary" }]` |
|
|
766
|
+
| `buildAlternateLinks(alternates)` | `buildAlternateLinks([{ hreflang: "en", href: "..." }])` | `[{ rel: "alternate", hreflang: "en", href: "..." }]` |
|
|
881
767
|
|
|
882
768
|
<br />
|
|
883
769
|
|
|
884
770
|
### JSON-LD Schema Generators
|
|
885
771
|
|
|
886
|
-
|
|
772
|
+
All return a plain object with `@context: "https://schema.org"` and `@type` set.
|
|
887
773
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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>
|
|
774
|
+
| Function | Schema Type | Use Case |
|
|
775
|
+
|---|---|---|
|
|
776
|
+
| `createOrganizationSchema(input)` | Organization | Company info, logo, social links, contact |
|
|
777
|
+
| `createWebsiteSchema(input)` | WebSite | Site name, sitelinks searchbox |
|
|
778
|
+
| `createArticleSchema(input)` | Article | Blog posts, news articles, authors, dates |
|
|
779
|
+
| `createProductSchema(input)` | Product | E-commerce: price, brand, SKU, ratings, availability |
|
|
780
|
+
| `createBreadcrumbSchema(items)` | BreadcrumbList | Navigation hierarchy |
|
|
781
|
+
| `createFAQSchema(items)` | FAQPage | FAQ pages with question + answer pairs |
|
|
782
|
+
| `composeSchemas(...schemas)` | @graph | Combine multiple schemas into one JSON-LD block |
|
|
930
783
|
|
|
931
784
|
<br />
|
|
932
785
|
|
|
933
786
|
### Utilities
|
|
934
787
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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>
|
|
788
|
+
| Function | What It Does |
|
|
789
|
+
|---|---|
|
|
790
|
+
| `safeJsonLdSerialize(data)` | Serialize JSON-LD safely — escapes `<`, `>`, `&` to prevent XSS |
|
|
791
|
+
| `normalizeUrl(url)` | Trim whitespace, remove trailing slashes |
|
|
792
|
+
| `buildFullUrl(base, path?)` | Combine base URL with path |
|
|
793
|
+
| `omitEmpty(obj)` | Remove keys with `undefined`, `null`, or empty string values |
|
|
794
|
+
| `deepMerge(base, override)` | Deep-merge two objects (arrays replaced, not concatenated) |
|
|
961
795
|
|
|
962
796
|
<br />
|
|
963
797
|
|
|
@@ -996,11 +830,9 @@ Renders all SEO tags as React elements. Place inside `<head>`.
|
|
|
996
830
|
]}
|
|
997
831
|
additionalMetaTags={[
|
|
998
832
|
{ name: "author", content: "Jane Doe" },
|
|
999
|
-
{ property: "article:published_time", content: "2025-06-15" },
|
|
1000
833
|
]}
|
|
1001
834
|
additionalLinkTags={[
|
|
1002
835
|
{ rel: "icon", href: "/favicon.ico" },
|
|
1003
|
-
{ rel: "apple-touch-icon", href: "/apple-touch-icon.png", sizes: "180x180" },
|
|
1004
836
|
]}
|
|
1005
837
|
jsonLd={createArticleSchema({ headline: "...", url: "..." })}
|
|
1006
838
|
/>
|
|
@@ -1048,49 +880,26 @@ import type {
|
|
|
1048
880
|
|
|
1049
881
|
## Live Demo
|
|
1050
882
|
|
|
1051
|
-
|
|
883
|
+
The repo includes a **working Express SSR demo** with every feature:
|
|
1052
884
|
|
|
1053
885
|
```bash
|
|
1054
|
-
git clone https://github.com/Tonmoy01/react-ssr-seo.git
|
|
1055
|
-
cd react-ssr-seo
|
|
886
|
+
git clone https://github.com/Tonmoy01/react-ssr-seo-toolkit.git
|
|
887
|
+
cd react-ssr-seo-toolkit
|
|
1056
888
|
npm install
|
|
1057
889
|
npm run demo
|
|
1058
890
|
```
|
|
1059
891
|
|
|
1060
|
-
Then
|
|
892
|
+
Then visit [http://localhost:3000](http://localhost:3000):
|
|
1061
893
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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>
|
|
894
|
+
| URL | Page | SEO Features |
|
|
895
|
+
|---|---|---|
|
|
896
|
+
| `/` | Home | Organization + Website schema, hreflang, OG images |
|
|
897
|
+
| `/getting-started` | Getting Started | Installation guide with copy-paste examples |
|
|
898
|
+
| `/article` | Article | Article schema, breadcrumbs, multiple authors, Twitter cards |
|
|
899
|
+
| `/product` | Product | Product schema, pricing, ratings, availability |
|
|
900
|
+
| `/faq` | FAQ | FAQPage schema with Q&A pairs |
|
|
901
|
+
| `/noindex` | No-Index | Robots noindex directive |
|
|
902
|
+
| `/api` | API Reference | Complete function and type documentation |
|
|
1094
903
|
|
|
1095
904
|
> **Tip:** Right-click any page and **View Page Source** to see all SEO tags in the raw HTML.
|
|
1096
905
|
|
|
@@ -1121,41 +930,25 @@ npm run demo # run demo server
|
|
|
1121
930
|
|
|
1122
931
|
## Troubleshooting
|
|
1123
932
|
|
|
1124
|
-
|
|
1125
|
-
<summary><strong>"Cannot find module 'react-ssr-seo'"</strong></summary>
|
|
933
|
+
### "Cannot find module 'react-ssr-seo-toolkit'"
|
|
1126
934
|
|
|
1127
|
-
|
|
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>
|
|
935
|
+
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-toolkit/dist/index.js` directly.
|
|
1131
936
|
|
|
1132
|
-
|
|
1133
|
-
<summary><strong>Hydration mismatch warnings</strong></summary>
|
|
1134
|
-
|
|
1135
|
-
<br />
|
|
937
|
+
### Hydration mismatch warnings
|
|
1136
938
|
|
|
1137
939
|
`<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
940
|
|
|
1140
|
-
|
|
1141
|
-
<summary><strong>JSON-LD not appearing in page source</strong></summary>
|
|
941
|
+
### JSON-LD not appearing in page source
|
|
1142
942
|
|
|
1143
|
-
|
|
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>
|
|
943
|
+
Make sure `<JsonLd>` is inside `<head>` and rendered during SSR — not in a client-only `useEffect`.
|
|
1147
944
|
|
|
1148
|
-
|
|
1149
|
-
<summary><strong>TypeScript errors</strong></summary>
|
|
1150
|
-
|
|
1151
|
-
<br />
|
|
945
|
+
### TypeScript errors
|
|
1152
946
|
|
|
1153
947
|
All types are exported. Import them directly:
|
|
1154
948
|
|
|
1155
949
|
```tsx
|
|
1156
950
|
import type { SEOConfig, OpenGraphConfig } from "react-ssr-seo-toolkit";
|
|
1157
951
|
```
|
|
1158
|
-
</details>
|
|
1159
952
|
|
|
1160
953
|
<br />
|
|
1161
954
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-ssr-seo-toolkit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Framework-agnostic SEO utilities, metadata builders, structured data helpers, and React components for SSR applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"seo",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
27
|
-
"url": "https://github.com/Tonmoy01/react-ssr-seo.git"
|
|
27
|
+
"url": "https://github.com/Tonmoy01/react-ssr-seo-toolkit.git"
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://react-ssr-seo.tonmoykhan.site/",
|
|
30
30
|
"bugs": {
|
|
31
|
-
"url": "https://github.com/Tonmoy01/react-ssr-seo/issues"
|
|
31
|
+
"url": "https://github.com/Tonmoy01/react-ssr-seo-toolkit/issues"
|
|
32
32
|
},
|
|
33
33
|
"type": "module",
|
|
34
34
|
"main": "./dist/index.cjs",
|