react-ssr-seo-toolkit 1.0.2 → 1.0.4

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.
Files changed (2) hide show
  1. package/README.md +247 -414
  2. package/package.json +4 -4
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.1-000000?style=for-the-badge&labelColor=000000" alt="react-ssr-seo" />
5
+ <img src="https://img.shields.io/badge/react--ssr--seo--toolkit-v1.0.4-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
  &nbsp;
20
20
  [![License](https://img.shields.io/npm/l/react-ssr-seo-toolkit?style=for-the-badge&color=blue)](./LICENSE)
21
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)
22
+ [![Bundle](https://img.shields.io/bundlephobia/minzip/react-ssr-seo-toolkit?style=for-the-badge&label=size&color=success)](https://bundlephobia.com/package/react-ssr-seo-toolkit)
23
23
 
24
24
  <br />
25
25
 
@@ -98,56 +98,118 @@ All in one package. Zero dependencies. Fully typed. SSR-safe.
98
98
  npm install react-ssr-seo-toolkit
99
99
  ```
100
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
- ```
101
+ > **Requires:** `react >= 18.0.0` as a peer dependency. Zero other dependencies.
102
+
103
+ <br />
105
104
 
106
- > **Requires:** `react >= 18.0.0` as peer dependency
105
+ ### 2. Project Structure
106
+
107
+ The key idea: **pages never write `<html>` or `<head>` tags** — that's handled by a Document component, just like in Next.js or any modern React framework.
108
+
109
+ ```
110
+ my-app/
111
+ ├── config/
112
+ │ └── seo.ts ← site-wide SEO defaults
113
+ ├── components/
114
+ │ └── Document.tsx ← handles <html>, <head>, <SEOHead>, <body>
115
+ ├── pages/
116
+ │ ├── HomePage.tsx ← just content + SEO config (no <html> tags!)
117
+ │ ├── AboutPage.tsx
118
+ │ └── BlogPost.tsx
119
+ ├── server.tsx ← Express / SSR entry point
120
+ └── package.json
121
+ ```
107
122
 
108
123
  <br />
109
124
 
110
- ### 2. Create Site Config (once)
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
- openGraph: { siteName: "MySite", type: "website" },
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
- ### 3. Add to Any Page
147
+ ### 3.5. Create a Document Component
125
148
 
126
- ```tsx
127
- import { SEOHead, mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
149
+ The Document handles `<html>`, `<head>`, `<SEOHead>`, and `<body>` — so pages never have to.
128
150
 
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
- });
151
+ ```tsx
152
+ // components/Document.tsx
153
+ import { SEOHead, JsonLd } from "react-ssr-seo-toolkit";
154
+ import type { SEOConfig } from "react-ssr-seo-toolkit";
155
+
156
+ interface DocumentProps {
157
+ children: React.ReactNode;
158
+ seo: SEOConfig;
159
+ schemas?: Record<string, unknown>[];
160
+ }
135
161
 
162
+ export function Document({ children, seo, schemas }: DocumentProps) {
136
163
  return (
137
- <html>
164
+ <html lang="en">
138
165
  <head>
166
+ <meta charSet="utf-8" />
167
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
168
+ <link rel="icon" href="/favicon.ico" />
139
169
  <SEOHead {...seo} />
140
- {/* Renders: <title>, <meta>, <link>, <script type="application/ld+json"> */}
170
+ {schemas?.map((schema, i) => <JsonLd key={i} data={schema} />)}
141
171
  </head>
142
172
  <body>
143
- <h1>About Us</h1>
173
+ <nav>{/* shared navigation */}</nav>
174
+ <main>{children}</main>
175
+ <footer>{/* shared footer */}</footer>
144
176
  </body>
145
177
  </html>
146
178
  );
147
179
  }
148
180
  ```
149
181
 
150
- **Done.** That's all you need for basic SEO. Keep reading for real-world examples.
182
+ > This is the same pattern used by Next.js (`layout.tsx`), Remix (`root.tsx`), and React Router's root component. We call it `Document` to distinguish it from route-level layouts.
183
+
184
+ <br />
185
+
186
+ ### 4. Add to Any Page
187
+
188
+ Merge the shared config with page-specific values. **No `<html>` or `<head>` tags needed** — the Document handles that.
189
+
190
+ ```tsx
191
+ // pages/AboutPage.tsx
192
+ import { mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
193
+ import { siteConfig, SITE_URL } from "../config/seo";
194
+ import { Document } from "../components/Document";
195
+
196
+ export function AboutPage() {
197
+ const seo = mergeSEOConfig(siteConfig, {
198
+ title: "About Us",
199
+ description: "Learn about our company and mission.",
200
+ canonical: buildCanonicalUrl(SITE_URL, "/about"),
201
+ });
202
+
203
+ return (
204
+ <Document seo={seo}>
205
+ <h1>About Us</h1>
206
+ <p>Our story...</p>
207
+ </Document>
208
+ );
209
+ }
210
+ ```
211
+
212
+ **That's it.** You now have full SEO on every page. Keep reading for structured data and framework examples.
151
213
 
152
214
  <br />
153
215
 
@@ -163,29 +225,21 @@ function AboutPage() {
163
225
 
164
226
  ### Blog / Article Page
165
227
 
166
- <details open>
167
- <summary><strong>Click to expand full example</strong></summary>
168
-
169
228
  ```tsx
229
+ // pages/BlogPost.tsx
170
230
  import {
171
- SEOHead, JsonLd,
172
- createSEOConfig, mergeSEOConfig, buildCanonicalUrl,
231
+ mergeSEOConfig, buildCanonicalUrl,
173
232
  createArticleSchema, createBreadcrumbSchema,
174
233
  } from "react-ssr-seo-toolkit";
234
+ import { siteConfig, SITE_URL } from "../config/seo";
235
+ import { Document } from "../components/Document";
175
236
 
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() {
237
+ export function BlogPostPage() {
184
238
  // ── Page SEO ──────────────────────────────────────────────
185
239
  const seo = mergeSEOConfig(siteConfig, {
186
240
  title: "How to Build an SSR App",
187
241
  description: "A complete guide to building server-rendered React apps with proper SEO.",
188
- canonical: buildCanonicalUrl("https://myblog.com", "/blog/ssr-guide"),
242
+ canonical: buildCanonicalUrl(SITE_URL, "/blog/ssr-guide"),
189
243
  openGraph: {
190
244
  title: "How to Build an SSR App",
191
245
  description: "A complete guide to SSR with React.",
@@ -227,29 +281,21 @@ function BlogPostPage() {
227
281
  { name: "How to Build an SSR App", url: "https://myblog.com/blog/ssr-guide" },
228
282
  ]);
229
283
 
230
- // ── Render ────────────────────────────────────────────────
284
+ // ── Render — no <html> or <head> tags! ────────────────────
231
285
  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>
286
+ <Document seo={seo} schemas={[article, breadcrumbs]}>
287
+ <article>
288
+ <h1>How to Build an SSR App</h1>
289
+ <p>Your article content here...</p>
290
+ </article>
291
+ </Document>
245
292
  );
246
293
  }
247
294
  ```
248
295
 
249
- </details>
296
+ <br />
250
297
 
251
- <details>
252
- <summary><strong>See the HTML output this generates</strong></summary>
298
+ ### Generated HTML Output
253
299
 
254
300
  ```html
255
301
  <head>
@@ -257,40 +303,26 @@ function BlogPostPage() {
257
303
  <title>How to Build an SSR App | My Blog</title>
258
304
  <meta name="description" content="A complete guide to building server-rendered React apps..." />
259
305
  <link rel="canonical" href="https://myblog.com/blog/ssr-guide" />
260
- <meta name="robots" content="index, follow" />
261
306
 
262
- <!-- Open Graph (Facebook, LinkedIn, etc.) -->
307
+ <!-- Open Graph -->
263
308
  <meta property="og:title" content="How to Build an SSR App" />
264
309
  <meta property="og:description" content="A complete guide to SSR with React." />
265
310
  <meta property="og:type" content="article" />
266
311
  <meta property="og:url" content="https://myblog.com/blog/ssr-guide" />
267
312
  <meta property="og:site_name" content="My Blog" />
268
313
  <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
314
 
273
315
  <!-- Twitter Card -->
274
316
  <meta name="twitter:card" content="summary_large_image" />
275
317
  <meta name="twitter:site" content="@myblog" />
276
- <meta name="twitter:creator" content="@authorhandle" />
277
318
  <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
319
 
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>
320
+ <!-- JSON-LD -->
321
+ <script type="application/ld+json">{"@context":"https://schema.org","@type":"Article",...}</script>
322
+ <script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList",...}</script>
289
323
  </head>
290
324
  ```
291
325
 
292
- </details>
293
-
294
326
  <br />
295
327
 
296
328
  ---
@@ -299,21 +331,13 @@ function BlogPostPage() {
299
331
 
300
332
  ### E-Commerce Product Page
301
333
 
302
- <details open>
303
- <summary><strong>Click to expand full example</strong></summary>
304
-
305
334
  ```tsx
306
335
  import {
307
- SEOHead, JsonLd,
308
- createSEOConfig, mergeSEOConfig, buildCanonicalUrl,
309
- createProductSchema, createBreadcrumbSchema,
336
+ mergeSEOConfig, buildCanonicalUrl,
337
+ createProductSchema,
310
338
  } 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
- });
339
+ import { siteConfig, SITE_URL } from "../config/seo";
340
+ import { Document } from "../components/Document";
317
341
 
318
342
  function ProductPage() {
319
343
  const product = {
@@ -328,9 +352,8 @@ function ProductPage() {
328
352
  reviewCount: 342,
329
353
  };
330
354
 
331
- const url = buildCanonicalUrl("https://acmestore.com", "/products/ergonomic-keyboard");
355
+ const url = buildCanonicalUrl(SITE_URL, "/products/ergonomic-keyboard");
332
356
 
333
- // ── Page SEO ──────────────────────────────────────────────
334
357
  const seo = mergeSEOConfig(siteConfig, {
335
358
  title: product.name,
336
359
  description: product.description,
@@ -344,7 +367,6 @@ function ProductPage() {
344
367
  },
345
368
  });
346
369
 
347
- // ── Structured Data ───────────────────────────────────────
348
370
  const schema = createProductSchema({
349
371
  name: product.name,
350
372
  url,
@@ -359,31 +381,15 @@ function ProductPage() {
359
381
  reviewCount: product.reviewCount,
360
382
  });
361
383
 
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
384
  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>
385
+ <Document seo={seo} schemas={[schema]}>
386
+ <h1>{product.name}</h1>
387
+ <p>${product.price}</p>
388
+ </Document>
381
389
  );
382
390
  }
383
391
  ```
384
392
 
385
- </details>
386
-
387
393
  <br />
388
394
 
389
395
  ---
@@ -392,14 +398,12 @@ function ProductPage() {
392
398
 
393
399
  ### FAQ Page
394
400
 
395
- <details open>
396
- <summary><strong>Click to expand full example</strong></summary>
397
-
398
401
  ```tsx
399
402
  import {
400
- SEOHead, JsonLd,
401
403
  mergeSEOConfig, buildCanonicalUrl, createFAQSchema,
402
404
  } from "react-ssr-seo-toolkit";
405
+ import { siteConfig, SITE_URL } from "../config/seo";
406
+ import { Document } from "../components/Document";
403
407
 
404
408
  function FAQPage() {
405
409
  const faqs = [
@@ -411,31 +415,23 @@ function FAQPage() {
411
415
  const seo = mergeSEOConfig(siteConfig, {
412
416
  title: "FAQ",
413
417
  description: "Frequently asked questions about our products and services.",
414
- canonical: buildCanonicalUrl("https://mysite.com", "/faq"),
418
+ canonical: buildCanonicalUrl(SITE_URL, "/faq"),
415
419
  });
416
420
 
417
421
  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>
422
+ <Document seo={seo} schemas={[createFAQSchema(faqs)]}>
423
+ <h1>Frequently Asked Questions</h1>
424
+ {faqs.map((faq, i) => (
425
+ <details key={i}>
426
+ <summary>{faq.question}</summary>
427
+ <p>{faq.answer}</p>
428
+ </details>
429
+ ))}
430
+ </Document>
433
431
  );
434
432
  }
435
433
  ```
436
434
 
437
- </details>
438
-
439
435
  <br />
440
436
 
441
437
  ---
@@ -444,15 +440,13 @@ function FAQPage() {
444
440
 
445
441
  ### Homepage (Organization + Website Schema)
446
442
 
447
- <details>
448
- <summary><strong>Click to expand full example</strong></summary>
449
-
450
443
  ```tsx
451
444
  import {
452
- SEOHead, JsonLd,
453
445
  mergeSEOConfig,
454
446
  createOrganizationSchema, createWebsiteSchema,
455
447
  } from "react-ssr-seo-toolkit";
448
+ import { siteConfig } from "../config/seo";
449
+ import { Document } from "../components/Document";
456
450
 
457
451
  function HomePage() {
458
452
  const seo = mergeSEOConfig(siteConfig, {
@@ -492,22 +486,13 @@ function HomePage() {
492
486
  });
493
487
 
494
488
  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>
489
+ <Document seo={seo} schemas={[org, site]}>
490
+ <h1>Welcome to Acme</h1>
491
+ </Document>
505
492
  );
506
493
  }
507
494
  ```
508
495
 
509
- </details>
510
-
511
496
  <br />
512
497
 
513
498
  ---
@@ -524,7 +509,6 @@ const seo = mergeSEOConfig(siteConfig, {
524
509
  { hreflang: "en", href: "https://mysite.com/en/products" },
525
510
  { hreflang: "es", href: "https://mysite.com/es/products" },
526
511
  { hreflang: "fr", href: "https://mysite.com/fr/products" },
527
- { hreflang: "de", href: "https://mysite.com/de/products" },
528
512
  { hreflang: "x-default", href: "https://mysite.com/products" },
529
513
  ],
530
514
  });
@@ -532,7 +516,6 @@ const seo = mergeSEOConfig(siteConfig, {
532
516
  // Generates:
533
517
  // <link rel="alternate" hreflang="en" href="https://mysite.com/en/products" />
534
518
  // <link rel="alternate" hreflang="es" href="https://mysite.com/es/products" />
535
- // <link rel="alternate" hreflang="fr" href="https://mysite.com/fr/products" />
536
519
  // ...
537
520
  ```
538
521
 
@@ -605,9 +588,6 @@ const combined = composeSchemas(
605
588
 
606
589
  ### Next.js App Router
607
590
 
608
- <details open>
609
- <summary><strong>Using with <code>generateMetadata()</code></strong></summary>
610
-
611
591
  ```tsx
612
592
  // app/blog/[slug]/page.tsx
613
593
  import {
@@ -657,19 +637,15 @@ export default function BlogPost({ params }) {
657
637
  }
658
638
  ```
659
639
 
660
- </details>
661
-
662
640
  <br />
663
641
 
664
642
  ### Next.js Pages Router
665
643
 
666
- <details>
667
- <summary><strong>Using with <code>next/head</code></strong></summary>
668
-
669
644
  ```tsx
670
- // pages/about.tsx
645
+ // pages/about.tsx — no <html> tags, Next.js handles that
671
646
  import Head from "next/head";
672
647
  import { SEOHead, mergeSEOConfig } from "react-ssr-seo-toolkit";
648
+ import { siteConfig } from "../config/seo";
673
649
 
674
650
  export default function AboutPage() {
675
651
  const seo = mergeSEOConfig(siteConfig, {
@@ -691,108 +667,112 @@ export default function AboutPage() {
691
667
  }
692
668
  ```
693
669
 
694
- </details>
695
-
696
670
  <br />
697
671
 
698
672
  ### React Router 7 SSR
699
673
 
700
- <details>
701
- <summary><strong>Using in root document</strong></summary>
702
-
703
674
  ```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
- });
675
+ // app/root.tsx — only the root layout writes <html>
676
+ import { Outlet, useMatches } from "react-router";
677
+ import { SEOHead } from "react-ssr-seo-toolkit";
712
678
 
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
- });
679
+ export default function Root() {
680
+ const matches = useMatches();
681
+ const seo = matches.at(-1)?.data?.seo;
723
682
 
724
683
  return (
725
684
  <html lang="en">
726
685
  <head>
727
686
  <meta charSet="utf-8" />
728
687
  <meta name="viewport" content="width=device-width, initial-scale=1" />
729
- <SEOHead {...seo} />
688
+ {seo && <SEOHead {...seo} />}
730
689
  </head>
731
690
  <body>
732
- <h1>Welcome to Acme</h1>
691
+ <Outlet />
733
692
  </body>
734
693
  </html>
735
694
  );
736
695
  }
737
696
  ```
738
697
 
739
- </details>
698
+ ```tsx
699
+ // app/routes/about.tsx — page just provides SEO data + content
700
+ import { mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
701
+ import { siteConfig, SITE_URL } from "../config/seo";
702
+
703
+ export function loader() {
704
+ return {
705
+ seo: mergeSEOConfig(siteConfig, {
706
+ title: "About",
707
+ canonical: buildCanonicalUrl(SITE_URL, "/about"),
708
+ }),
709
+ };
710
+ }
711
+
712
+ export default function AboutPage() {
713
+ return (
714
+ <main>
715
+ <h1>About Us</h1>
716
+ </main>
717
+ );
718
+ }
719
+ ```
740
720
 
741
721
  <br />
742
722
 
743
723
  ### Express + React SSR
744
724
 
745
- <details>
746
- <summary><strong>Using with <code>renderToString()</code></strong></summary>
747
-
748
725
  ```tsx
726
+ // server.tsx — renders page components that include Document internally
749
727
  import express from "express";
750
728
  import { renderToString } from "react-dom/server";
751
- import { SEOHead, JsonLd, createSEOConfig, mergeSEOConfig, createProductSchema } from "react-ssr-seo-toolkit";
729
+ import { HomePage } from "./pages/HomePage";
730
+ import { ProductPage } from "./pages/ProductPage";
752
731
 
753
732
  const app = express();
754
733
 
755
- const siteConfig = createSEOConfig({
756
- titleTemplate: "%s | My Store",
757
- openGraph: { siteName: "My Store" },
734
+ app.get("/", (req, res) => {
735
+ const html = renderToString(<HomePage />);
736
+ res.send(`<!DOCTYPE html>${html}`);
758
737
  });
759
738
 
760
- function ProductPage({ product }) {
739
+ app.get("/products/:id", (req, res) => {
740
+ const product = getProduct(req.params.id);
741
+ const html = renderToString(<ProductPage product={product} />);
742
+ res.send(`<!DOCTYPE html>${html}`);
743
+ });
744
+
745
+ app.listen(3000);
746
+ ```
747
+
748
+ ```tsx
749
+ // pages/ProductPage.tsx — no <html> tags, Document handles that
750
+ import { mergeSEOConfig, createProductSchema } from "react-ssr-seo-toolkit";
751
+ import { siteConfig } from "../config/seo";
752
+ import { Document } from "../components/Document";
753
+
754
+ export function ProductPage({ product }) {
761
755
  const seo = mergeSEOConfig(siteConfig, {
762
756
  title: product.name,
763
757
  description: product.description,
764
758
  canonical: product.url,
765
759
  });
766
760
 
761
+ const schema = createProductSchema({
762
+ name: product.name,
763
+ url: product.url,
764
+ price: product.price,
765
+ });
766
+
767
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>
768
+ <Document seo={seo} schemas={[schema]}>
769
+ <h1>{product.name}</h1>
770
+ <p>${product.price}</p>
771
+ </Document>
782
772
  );
783
773
  }
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
774
  ```
793
775
 
794
- </details>
795
-
796
776
  <br />
797
777
 
798
778
  ---
@@ -803,161 +783,55 @@ app.listen(3000);
803
783
 
804
784
  ### Config Builders
805
785
 
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>
786
+ | Function | What It Does |
787
+ |---|---|
788
+ | `createSEOConfig(config?)` | Create a normalized SEO config. Use for site-wide defaults. |
789
+ | `mergeSEOConfig(base, override)` | Deep-merge site config with page-level overrides. Arrays are replaced, not concatenated. |
790
+ | `normalizeSEOConfig(config)` | Trim strings, normalize URLs, clean up a config object. |
824
791
 
825
792
  <br />
826
793
 
827
794
  ### Metadata Helpers
828
795
 
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>
796
+ | Function | Example | Result |
797
+ |---|---|---|
798
+ | `buildTitle(title, template)` | `buildTitle("About", "%s \| MySite")` | `"About \| MySite"` |
799
+ | `buildDescription(desc, maxLen)` | `buildDescription("Long text...", 160)` | Truncated at 160 chars |
800
+ | `buildCanonicalUrl(base, path)` | `buildCanonicalUrl("https://x.com", "/about")` | `"https://x.com/about"` |
801
+ | `buildRobotsDirectives(config)` | `buildRobotsDirectives({ index: false })` | `"noindex, follow"` |
802
+ | `noIndex()` | `noIndex()` | `{ index: false, follow: true }` |
803
+ | `noIndexNoFollow()` | `noIndexNoFollow()` | `{ index: false, follow: false }` |
804
+ | `buildOpenGraph(config)` | `buildOpenGraph({ title: "Hi" })` | `[{ property: "og:title", content: "Hi" }]` |
805
+ | `buildTwitterMetadata(config)` | `buildTwitterMetadata({ card: "summary" })` | `[{ name: "twitter:card", content: "summary" }]` |
806
+ | `buildAlternateLinks(alternates)` | `buildAlternateLinks([{ hreflang: "en", href: "..." }])` | `[{ rel: "alternate", hreflang: "en", href: "..." }]` |
881
807
 
882
808
  <br />
883
809
 
884
810
  ### JSON-LD Schema Generators
885
811
 
886
- > All return a plain object with `@context: "https://schema.org"` and `@type` set.
812
+ All return a plain object with `@context: "https://schema.org"` and `@type` set.
887
813
 
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>
814
+ | Function | Schema Type | Use Case |
815
+ |---|---|---|
816
+ | `createOrganizationSchema(input)` | Organization | Company info, logo, social links, contact |
817
+ | `createWebsiteSchema(input)` | WebSite | Site name, sitelinks searchbox |
818
+ | `createArticleSchema(input)` | Article | Blog posts, news articles, authors, dates |
819
+ | `createProductSchema(input)` | Product | E-commerce: price, brand, SKU, ratings, availability |
820
+ | `createBreadcrumbSchema(items)` | BreadcrumbList | Navigation hierarchy |
821
+ | `createFAQSchema(items)` | FAQPage | FAQ pages with question + answer pairs |
822
+ | `composeSchemas(...schemas)` | @graph | Combine multiple schemas into one JSON-LD block |
930
823
 
931
824
  <br />
932
825
 
933
826
  ### Utilities
934
827
 
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>
828
+ | Function | What It Does |
829
+ |---|---|
830
+ | `safeJsonLdSerialize(data)` | Serialize JSON-LD safely — escapes `<`, `>`, `&` to prevent XSS |
831
+ | `normalizeUrl(url)` | Trim whitespace, remove trailing slashes |
832
+ | `buildFullUrl(base, path?)` | Combine base URL with path |
833
+ | `omitEmpty(obj)` | Remove keys with `undefined`, `null`, or empty string values |
834
+ | `deepMerge(base, override)` | Deep-merge two objects (arrays replaced, not concatenated) |
961
835
 
962
836
  <br />
963
837
 
@@ -996,11 +870,9 @@ Renders all SEO tags as React elements. Place inside `<head>`.
996
870
  ]}
997
871
  additionalMetaTags={[
998
872
  { name: "author", content: "Jane Doe" },
999
- { property: "article:published_time", content: "2025-06-15" },
1000
873
  ]}
1001
874
  additionalLinkTags={[
1002
875
  { rel: "icon", href: "/favicon.ico" },
1003
- { rel: "apple-touch-icon", href: "/apple-touch-icon.png", sizes: "180x180" },
1004
876
  ]}
1005
877
  jsonLd={createArticleSchema({ headline: "...", url: "..." })}
1006
878
  />
@@ -1048,49 +920,26 @@ import type {
1048
920
 
1049
921
  ## Live Demo
1050
922
 
1051
- Try it locally — the repo includes a **working Express SSR demo** with 5 pages:
923
+ The repo includes a **working Express SSR demo** with every feature:
1052
924
 
1053
925
  ```bash
1054
- git clone https://github.com/Tonmoy01/react-ssr-seo.git
1055
- cd react-ssr-seo
926
+ git clone https://github.com/Tonmoy01/react-ssr-seo-toolkit.git
927
+ cd react-ssr-seo-toolkit
1056
928
  npm install
1057
929
  npm run demo
1058
930
  ```
1059
931
 
1060
- Then open your browser:
932
+ Then visit [http://localhost:3000](http://localhost:3000):
1061
933
 
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>
934
+ | URL | Page | SEO Features |
935
+ |---|---|---|
936
+ | `/` | Home | Organization + Website schema, hreflang, OG images |
937
+ | `/getting-started` | Getting Started | Installation guide with copy-paste examples |
938
+ | `/article` | Article | Article schema, breadcrumbs, multiple authors, Twitter cards |
939
+ | `/product` | Product | Product schema, pricing, ratings, availability |
940
+ | `/faq` | FAQ | FAQPage schema with Q&A pairs |
941
+ | `/noindex` | No-Index | Robots noindex directive |
942
+ | `/api` | API Reference | Complete function and type documentation |
1094
943
 
1095
944
  > **Tip:** Right-click any page and **View Page Source** to see all SEO tags in the raw HTML.
1096
945
 
@@ -1121,41 +970,25 @@ npm run demo # run demo server
1121
970
 
1122
971
  ## Troubleshooting
1123
972
 
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>
973
+ ### "Cannot find module 'react-ssr-seo-toolkit'"
1131
974
 
1132
- <details>
1133
- <summary><strong>Hydration mismatch warnings</strong></summary>
975
+ 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.
1134
976
 
1135
- <br />
977
+ ### Hydration mismatch warnings
1136
978
 
1137
979
  `<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
980
 
1140
- <details>
1141
- <summary><strong>JSON-LD not appearing in page source</strong></summary>
981
+ ### JSON-LD not appearing in page source
1142
982
 
1143
- <br />
983
+ Make sure `<JsonLd>` is inside `<head>` and rendered during SSR — not in a client-only `useEffect`.
1144
984
 
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 />
985
+ ### TypeScript errors
1152
986
 
1153
987
  All types are exported. Import them directly:
1154
988
 
1155
989
  ```tsx
1156
990
  import type { SEOConfig, OpenGraphConfig } from "react-ssr-seo-toolkit";
1157
991
  ```
1158
- </details>
1159
992
 
1160
993
  <br />
1161
994
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-ssr-seo-toolkit",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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",
@@ -79,7 +79,7 @@
79
79
  "lint": "tsc --noEmit",
80
80
  "prepublishOnly": "npm run build",
81
81
  "clean": "rm -rf dist",
82
- "demo": "tsx examples/dev-server/server.tsx",
82
+ "demo": "tsx --watch examples/dev-server/server.tsx",
83
83
  "demo:build": "tsup examples/dev-server/server.tsx --format esm --platform node --out-dir demo-dist --no-splitting --no-dts --external express --external react --external react-dom --external react/jsx-runtime",
84
84
  "demo:start": "node demo-dist/server.js"
85
85
  },