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/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
+ [![npm](https://img.shields.io/npm/v/react-ssr-seo-toolkit?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/react-ssr-seo-toolkit)
17
+ &nbsp;
18
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
19
+ &nbsp;
20
+ [![License](https://img.shields.io/npm/l/react-ssr-seo-toolkit?style=for-the-badge&color=blue)](./LICENSE)
21
+ &nbsp;
22
+ [![Bundle](https://img.shields.io/bundlephobia/minzip/react-ssr-seo?style=for-the-badge&label=size&color=success)](https://bundlephobia.com/package/react-ssr-seo-toolkit)
23
+
24
+ <br />
25
+
26
+ **Meta Tags** &bull; **Open Graph** &bull; **Twitter Cards** &bull; **JSON-LD** &bull; **Canonical URLs** &bull; **Hreflang** &bull; **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/) &nbsp;&nbsp;|&nbsp;&nbsp; [Get Started](#-get-started) &nbsp;&nbsp;|&nbsp;&nbsp; [Examples](#-real-world-examples) &nbsp;&nbsp;|&nbsp;&nbsp; [API](#-api-reference) &nbsp;&nbsp;|&nbsp;&nbsp; [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>&lt;script&gt;</code> tags — escapes <code>&lt;</code>, <code>&gt;</code>, <code>&amp;</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** &bull; Made by [Tonmoy](https://github.com/Tonmoy01)
1183
+
1184
+ <br />
1185
+
1186
+ </div>