react-ssr-seo-toolkit 1.0.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +633 -460
  2. package/dist/adapters/nextjs.cjs +2 -0
  3. package/dist/adapters/nextjs.cjs.map +1 -0
  4. package/dist/adapters/nextjs.d.cts +86 -0
  5. package/dist/adapters/nextjs.d.ts +86 -0
  6. package/dist/adapters/nextjs.js +2 -0
  7. package/dist/adapters/nextjs.js.map +1 -0
  8. package/dist/adapters/react-router.cjs +2 -0
  9. package/dist/adapters/react-router.cjs.map +1 -0
  10. package/dist/adapters/react-router.d.cts +71 -0
  11. package/dist/adapters/react-router.d.ts +71 -0
  12. package/dist/adapters/react-router.js +2 -0
  13. package/dist/adapters/react-router.js.map +1 -0
  14. package/dist/chunk-3UCLUFOI.js +5 -0
  15. package/dist/chunk-3UCLUFOI.js.map +1 -0
  16. package/dist/chunk-A62THBIS.cjs +3 -0
  17. package/dist/chunk-A62THBIS.cjs.map +1 -0
  18. package/dist/chunk-FKDMHECL.cjs +2 -0
  19. package/dist/chunk-FKDMHECL.cjs.map +1 -0
  20. package/dist/chunk-GNLXGAS6.js +3 -0
  21. package/dist/chunk-GNLXGAS6.js.map +1 -0
  22. package/dist/chunk-LEOORZQY.cjs +5 -0
  23. package/dist/chunk-LEOORZQY.cjs.map +1 -0
  24. package/dist/chunk-LP66SUGR.js +2 -0
  25. package/dist/chunk-LP66SUGR.js.map +1 -0
  26. package/dist/chunk-ROA74LH6.cjs +61 -0
  27. package/dist/chunk-ROA74LH6.cjs.map +1 -0
  28. package/dist/chunk-WM5VVPED.js +61 -0
  29. package/dist/chunk-WM5VVPED.js.map +1 -0
  30. package/dist/chunk-X535MU7Z.js +5 -0
  31. package/dist/chunk-X535MU7Z.js.map +1 -0
  32. package/dist/chunk-ZADUJHVR.cjs +5 -0
  33. package/dist/chunk-ZADUJHVR.cjs.map +1 -0
  34. package/dist/chunk-ZECSTNEE.cjs +3 -0
  35. package/dist/chunk-ZECSTNEE.cjs.map +1 -0
  36. package/dist/chunk-ZVFEGG25.js +3 -0
  37. package/dist/chunk-ZVFEGG25.js.map +1 -0
  38. package/dist/components.cjs +1 -1
  39. package/dist/components.d.cts +2 -2
  40. package/dist/components.d.ts +2 -2
  41. package/dist/components.js +1 -1
  42. package/dist/index-Br7Sh9Ur.d.cts +329 -0
  43. package/dist/index-Br7Sh9Ur.d.ts +329 -0
  44. package/dist/{index-DAGfo2Fc.d.ts → index-CKNcgAj8.d.ts} +10 -2
  45. package/dist/{index-RBSUcdqN.d.cts → index-Wgogf4CX.d.cts} +10 -2
  46. package/dist/index.cjs +1 -1
  47. package/dist/index.d.cts +7 -4
  48. package/dist/index.d.ts +7 -4
  49. package/dist/index.js +1 -1
  50. package/dist/og.cjs +2 -0
  51. package/dist/og.cjs.map +1 -0
  52. package/dist/og.d.cts +5 -0
  53. package/dist/og.d.ts +5 -0
  54. package/dist/og.js +2 -0
  55. package/dist/og.js.map +1 -0
  56. package/dist/schema.cjs +1 -1
  57. package/dist/schema.d.cts +6 -2
  58. package/dist/schema.d.ts +6 -2
  59. package/dist/schema.js +1 -1
  60. package/dist/sitemap.cjs +2 -0
  61. package/dist/sitemap.cjs.map +1 -0
  62. package/dist/sitemap.d.cts +12 -0
  63. package/dist/sitemap.d.ts +12 -0
  64. package/dist/sitemap.js +2 -0
  65. package/dist/sitemap.js.map +1 -0
  66. package/dist/validation.cjs +2 -0
  67. package/dist/validation.cjs.map +1 -0
  68. package/dist/validation.d.cts +8 -0
  69. package/dist/validation.d.ts +8 -0
  70. package/dist/validation.js +2 -0
  71. package/dist/validation.js.map +1 -0
  72. package/package.json +58 -2
  73. package/dist/chunk-63ETSZTD.cjs +0 -3
  74. package/dist/chunk-63ETSZTD.cjs.map +0 -1
  75. package/dist/chunk-ES4OXVOR.js +0 -3
  76. package/dist/chunk-ES4OXVOR.js.map +0 -1
  77. package/dist/chunk-QBHCTDUJ.cjs +0 -2
  78. package/dist/chunk-QBHCTDUJ.cjs.map +0 -1
  79. package/dist/chunk-YMCW2G4X.js +0 -2
  80. package/dist/chunk-YMCW2G4X.js.map +0 -1
  81. package/dist/index-Dr2yktvz.d.cts +0 -136
  82. package/dist/index-Dr2yktvz.d.ts +0 -136
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <br />
4
4
 
5
- <img src="https://img.shields.io/badge/react--ssr--seo--toolkit-v1.0.5-000000?style=for-the-badge&labelColor=000000" alt="react-ssr-seo-toolkit" />
5
+ <img src="https://img.shields.io/badge/react--ssr--seo--toolkit-v1.2.0-000000?style=for-the-badge&labelColor=000000" alt="react-ssr-seo-toolkit" />
6
6
 
7
7
  <br />
8
8
  <br />
@@ -23,7 +23,7 @@
23
23
 
24
24
  <br />
25
25
 
26
- **Meta Tags** &bull; **Open Graph** &bull; **Twitter Cards** &bull; **JSON-LD** &bull; **Canonical URLs** &bull; **Hreflang** &bull; **Robots**
26
+ **Meta Tags** &bull; **Open Graph** &bull; **Twitter Cards** &bull; **JSON-LD** &bull; **Canonical URLs** &bull; **Hreflang** &bull; **Robots** &bull; **Sitemap** &bull; **SEO Validation** &bull; **OG Image**
27
27
 
28
28
  All in one package. Zero dependencies. Fully typed. SSR-safe.
29
29
 
@@ -50,8 +50,8 @@ All in one package. Zero dependencies. Fully typed. SSR-safe.
50
50
  - Most SEO packages are **locked to Next.js**
51
51
  - Many rely on **browser-only APIs** (`window`, `document`)
52
52
  - JSON-LD usually needs a **separate package**
53
- - Hard to get **type safety** across meta tags
54
- - Hydration mismatches in SSR
53
+ - No sitemap or robots.txt generation
54
+ - No way to validate SEO completeness at build time
55
55
 
56
56
  </td>
57
57
  <td width="50%">
@@ -60,9 +60,11 @@ All in one package. Zero dependencies. Fully typed. SSR-safe.
60
60
 
61
61
  - **Framework-agnostic** — works everywhere
62
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
63
+ - **JSON-LD built-in** — 9 schema types including Person, Recipe, JobPosting
64
+ - **Sitemap + robots.txt** generator built-in
65
+ - **SEO validation + scoring** — catch missing tags at build time
66
+ - **OG image SVG generator** — no external service needed
67
+ - **Social preview component** — see how your page looks on Twitter, Facebook, etc.
66
68
 
67
69
  </td>
68
70
  </tr>
@@ -76,9 +78,9 @@ All in one package. Zero dependencies. Fully typed. SSR-safe.
76
78
 
77
79
  | | Framework | Integration |
78
80
  |:---:|---|---|
79
- | <img src="https://img.shields.io/badge/-Next.js-000?style=flat-square&logo=nextdotjs" /> | **Next.js App Router** | `generateMetadata()` + `safeJsonLdSerialize()` |
81
+ | <img src="https://img.shields.io/badge/-Next.js-000?style=flat-square&logo=nextdotjs" /> | **Next.js App Router** | `toNextMetadata()` adapter `generateMetadata()` |
80
82
  | <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 |
83
+ | <img src="https://img.shields.io/badge/-React_Router-CA4245?style=flat-square&logo=reactrouter&logoColor=white" /> | **React Router 7** | `toRouterMeta()` adapter `meta()` export |
82
84
  | <img src="https://img.shields.io/badge/-Express-000?style=flat-square&logo=express" /> | **Express + React SSR** | `<SEOHead>` in `renderToString()` |
83
85
  | <img src="https://img.shields.io/badge/-Remix-000?style=flat-square&logo=remix" /> | **Remix / Astro / Solid** | Pure utility functions (no React needed) |
84
86
 
@@ -124,14 +126,12 @@ my-app/
124
126
 
125
127
  ### 3. Create Site Config (once)
126
128
 
127
- This file holds defaults that every page inherits. Pages override only what they need.
128
-
129
129
  ```tsx
130
130
  // config/seo.ts
131
131
  import { createSEOConfig } from "react-ssr-seo-toolkit";
132
132
 
133
133
  export const siteConfig = createSEOConfig({
134
- titleTemplate: "%s | MySite", // auto-appends " | MySite" to every page title
134
+ titleTemplate: "%s | MySite",
135
135
  description: "Default site description for SEO.",
136
136
  openGraph: { siteName: "MySite", type: "website", locale: "en_US" },
137
137
  twitter: { card: "summary_large_image", site: "@mysite" },
@@ -140,14 +140,10 @@ export const siteConfig = createSEOConfig({
140
140
  export const SITE_URL = "https://mysite.com";
141
141
  ```
142
142
 
143
- > **Tip:** `titleTemplate` uses `%s` as a placeholder. Setting `title: "About"` renders as `About | MySite`.
144
-
145
143
  <br />
146
144
 
147
145
  ### 3.5. Create a Document Component
148
146
 
149
- The Document handles `<html>`, `<head>`, `<SEOHead>`, and `<body>` — so pages never have to.
150
-
151
147
  ```tsx
152
148
  // components/Document.tsx
153
149
  import { SEOHead, JsonLd } from "react-ssr-seo-toolkit";
@@ -179,14 +175,10 @@ export function Document({ children, seo, schemas }: DocumentProps) {
179
175
  }
180
176
  ```
181
177
 
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
178
  <br />
185
179
 
186
180
  ### 4. Add to Any Page
187
181
 
188
- Merge the shared config with page-specific values. **No `<html>` or `<head>` tags needed** — the Document handles that.
189
-
190
182
  ```tsx
191
183
  // pages/AboutPage.tsx
192
184
  import { mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
@@ -209,7 +201,7 @@ export function AboutPage() {
209
201
  }
210
202
  ```
211
203
 
212
- **That's it.** You now have full SEO on every page. Keep reading for structured data and framework examples.
204
+ **That's it.** Keep reading for sitemap generation, SEO validation, OG images, and more.
213
205
 
214
206
  <br />
215
207
 
@@ -219,14 +211,316 @@ export function AboutPage() {
219
211
 
220
212
  ## Real-World Examples
221
213
 
222
- > Every example below is **copy-paste ready**. Just change the URLs and content.
214
+ <br />
215
+
216
+ ### Sitemap Generation
217
+
218
+ ```ts
219
+ // scripts/generate-sitemap.ts
220
+ import { generateSitemap } from "react-ssr-seo-toolkit/sitemap";
221
+ import { writeFileSync } from "fs";
222
+
223
+ const sitemap = generateSitemap({
224
+ baseUrl: "https://trustix.uk",
225
+ routes: [
226
+ { path: "/", priority: 1.0, changefreq: "daily" },
227
+ { path: "/tickets", priority: 0.9, changefreq: "hourly" },
228
+ { path: "/about", priority: 0.5, changefreq: "monthly" },
229
+ "/blog",
230
+ "/contact",
231
+ ],
232
+ exclude: ["/dashboard/*", "/admin/*", "/login"],
233
+ defaultChangefreq: "weekly",
234
+ defaultPriority: 0.7,
235
+ });
236
+
237
+ writeFileSync("public/sitemap.xml", sitemap);
238
+ ```
239
+
240
+ **Output:**
241
+
242
+ ```xml
243
+ <?xml version="1.0" encoding="UTF-8"?>
244
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
245
+ <url>
246
+ <loc>https://trustix.uk</loc>
247
+ <lastmod>2026-05-08</lastmod>
248
+ <changefreq>daily</changefreq>
249
+ <priority>1.0</priority>
250
+ </url>
251
+ ...
252
+ </urlset>
253
+ ```
254
+
255
+ > **Tip:** Call this in a build script or as an Express endpoint: `res.type("application/xml").send(sitemap)`.
256
+
257
+ <br />
258
+
259
+ ---
260
+
261
+ <br />
262
+
263
+ ### robots.txt Generation
264
+
265
+ ```ts
266
+ import { generateRobots } from "react-ssr-seo-toolkit/sitemap";
267
+
268
+ const robots = generateRobots({
269
+ rules: [
270
+ {
271
+ userAgent: "*",
272
+ allow: "/",
273
+ disallow: ["/dashboard", "/admin", "/login"],
274
+ },
275
+ {
276
+ userAgent: "Googlebot",
277
+ allow: "/",
278
+ crawlDelay: 2,
279
+ },
280
+ ],
281
+ sitemap: "https://trustix.uk/sitemap.xml",
282
+ });
283
+ ```
284
+
285
+ **Output:**
286
+
287
+ ```
288
+ User-agent: *
289
+ Allow: /
290
+ Disallow: /dashboard
291
+ Disallow: /admin
292
+ Disallow: /login
293
+
294
+ User-agent: Googlebot
295
+ Allow: /
296
+ Crawl-delay: 2
297
+
298
+ Sitemap: https://trustix.uk/sitemap.xml
299
+ ```
300
+
301
+ <br />
302
+
303
+ ---
304
+
305
+ <br />
306
+
307
+ ### Breadcrumb Auto-Generation from URL
308
+
309
+ ```ts
310
+ import { autoBreadcrumb } from "react-ssr-seo-toolkit/sitemap";
311
+ import { createBreadcrumbSchema } from "react-ssr-seo-toolkit";
312
+
313
+ // /ticket/liverpool-vs-arsenal → breadcrumb items automatically
314
+ const items = autoBreadcrumb("/ticket/liverpool-vs-arsenal", {
315
+ baseUrl: "https://trustix.uk",
316
+ labels: { "/ticket": "Tickets" }, // optional custom label overrides
317
+ });
318
+
319
+ // Result:
320
+ // [
321
+ // { name: "Home", url: "https://trustix.uk/" },
322
+ // { name: "Tickets", url: "https://trustix.uk/ticket" },
323
+ // { name: "Liverpool Vs Arsenal", url: "https://trustix.uk/ticket/liverpool-vs-arsenal" },
324
+ // ]
325
+
326
+ // Feed directly into schema:
327
+ const schema = createBreadcrumbSchema(items);
328
+ ```
329
+
330
+ <br />
331
+
332
+ ---
333
+
334
+ <br />
335
+
336
+ ### SEO Validation (build-time warnings)
337
+
338
+ Catch missing tags before they reach production:
339
+
340
+ ```ts
341
+ import { validateSEO, printValidationReport } from "react-ssr-seo-toolkit/validation";
342
+
343
+ const issues = validateSEO([
344
+ {
345
+ path: "/",
346
+ name: "HomePage",
347
+ seo: { title: "Trustix UK", description: "Buy tickets online." },
348
+ },
349
+ {
350
+ path: "/tickets",
351
+ name: "TicketListPage",
352
+ seo: {
353
+ title: "Tickets",
354
+ description: "Browse all tickets.",
355
+ canonical: "https://trustix.uk/tickets",
356
+ twitter: { card: "summary_large_image" },
357
+ // og:image missing — will warn
358
+ },
359
+ },
360
+ ]);
361
+
362
+ console.log(printValidationReport(issues));
363
+ ```
364
+
365
+ **Console output:**
366
+
367
+ ```
368
+ HomePage:
369
+ ❌ [canonical] Missing canonical URL
370
+ ⚠️ [og:image] Missing og:image
371
+ ⚠️ [twitter:card] Missing twitter:card
372
+ ⚠️ [structured-data] No structured data (JSON-LD)
373
+
374
+ TicketListPage:
375
+ ⚠️ [og:image] Missing og:image
376
+ ⚠️ [structured-data] No structured data (JSON-LD)
377
+ ```
378
+
379
+ <br />
380
+
381
+ ---
382
+
383
+ <br />
384
+
385
+ ### SEO Score per Page
386
+
387
+ ```ts
388
+ import { getSEOScore, formatSEOScore } from "react-ssr-seo-toolkit/validation";
389
+
390
+ const result = getSEOScore(
391
+ {
392
+ title: "Liverpool vs Arsenal — Premier League Tickets",
393
+ description: "Buy tickets for Liverpool vs Arsenal at Anfield.",
394
+ canonical: "https://trustix.uk/tickets/liverpool-arsenal",
395
+ openGraph: {
396
+ images: [{ url: "https://trustix.uk/og/liverpool-arsenal.jpg" }],
397
+ },
398
+ twitter: { card: "summary_large_image" },
399
+ jsonLd: { "@context": "https://schema.org", "@type": "Event" },
400
+ },
401
+ "TicketDetailPage"
402
+ );
403
+
404
+ console.log(formatSEOScore(result));
405
+ ```
406
+
407
+ **Console output:**
408
+
409
+ ```
410
+ 🟢 SEO Score: TicketDetailPage 75/100 (75%)
411
+ ✅ Title
412
+ ✅ Description
413
+ ✅ Canonical URL
414
+ ✅ og:image
415
+ ✅ og:title
416
+ ✅ og:description
417
+ ✅ Twitter Card
418
+ ✅ Structured Data
419
+ ❌ Robots Directives (-5) — No robots directives
420
+ ❌ Hreflang (-5) — No hreflang alternates
421
+ ```
422
+
423
+ <br />
424
+
425
+ ---
426
+
427
+ <br />
428
+
429
+ ### OG Image Generation (no external service)
430
+
431
+ Generate 1200×630 SVG images server-side — no Puppeteer, no Vercel Edge, no API keys:
432
+
433
+ ```ts
434
+ import { createOGImageSVG } from "react-ssr-seo-toolkit/og";
435
+
436
+ // Default template — dark gradient background
437
+ const svg = createOGImageSVG({
438
+ title: "How to Build an SSR App",
439
+ description: "A complete guide to server-rendered React.",
440
+ brand: "My Blog",
441
+ accentColor: "#6366f1",
442
+ });
443
+
444
+ // Serve from an Express endpoint:
445
+ app.get("/og/default.svg", (req, res) => {
446
+ res.type("image/svg+xml").send(
447
+ createOGImageSVG({ title: req.query.title as string, brand: "My Blog" })
448
+ );
449
+ });
450
+ ```
451
+
452
+ **Article template** (light background with category tag):
453
+
454
+ ```ts
455
+ const svg = createOGImageSVG({
456
+ title: "Premier League Preview 2026",
457
+ description: "Everything you need to know about the upcoming season.",
458
+ template: "article",
459
+ category: "Sports",
460
+ author: "James Walker",
461
+ dateString: "May 8, 2026",
462
+ brand: "Trustix UK",
463
+ accentColor: "#e63946",
464
+ });
465
+ ```
466
+
467
+ **Sports event template** (VS layout):
468
+
469
+ ```ts
470
+ const svg = createOGImageSVG({
471
+ title: "Liverpool vs Arsenal",
472
+ template: "sports-event",
473
+ homeTeam: "Liverpool",
474
+ awayTeam: "Arsenal",
475
+ eventDate: "Saturday, May 15, 2026 · 17:30",
476
+ brand: "Trustix UK",
477
+ accentColor: "#ffd700",
478
+ });
479
+ ```
480
+
481
+ > The SVG output can be used directly as `og:image` or converted to PNG with `sharp`:
482
+ > ```ts
483
+ > import sharp from "sharp";
484
+ > const png = await sharp(Buffer.from(svg)).png().toBuffer();
485
+ > ```
486
+
487
+ <br />
488
+
489
+ ---
490
+
491
+ <br />
492
+
493
+ ### Social Preview Component (dev mode)
494
+
495
+ See exactly how your page will look on Twitter, Facebook, LinkedIn, and Google — without leaving the browser:
496
+
497
+ ```tsx
498
+ import { SEOPreview } from "react-ssr-seo-toolkit/components";
499
+
500
+ // Twitter card preview
501
+ <SEOPreview config={seoConfig} platform="twitter" />
502
+
503
+ // Facebook / Open Graph preview
504
+ <SEOPreview config={seoConfig} platform="facebook" />
505
+
506
+ // LinkedIn preview
507
+ <SEOPreview config={seoConfig} platform="linkedin" />
508
+
509
+ // Google search result preview
510
+ <SEOPreview config={seoConfig} platform="google" />
511
+ ```
512
+
513
+ > **Tip:** Wrap in `{process.env.NODE_ENV === "development" && ...}` so it never ships to production.
514
+
515
+ <br />
516
+
517
+ ---
223
518
 
224
519
  <br />
225
520
 
226
521
  ### Blog / Article Page
227
522
 
228
523
  ```tsx
229
- // pages/BlogPost.tsx
230
524
  import {
231
525
  mergeSEOConfig, buildCanonicalUrl,
232
526
  createArticleSchema, createBreadcrumbSchema,
@@ -235,40 +529,25 @@ import { siteConfig, SITE_URL } from "../config/seo";
235
529
  import { Document } from "../components/Document";
236
530
 
237
531
  export function BlogPostPage() {
238
- // ── Page SEO ──────────────────────────────────────────────
239
532
  const seo = mergeSEOConfig(siteConfig, {
240
533
  title: "How to Build an SSR App",
241
534
  description: "A complete guide to building server-rendered React apps with proper SEO.",
242
535
  canonical: buildCanonicalUrl(SITE_URL, "/blog/ssr-guide"),
243
536
  openGraph: {
244
537
  title: "How to Build an SSR App",
245
- description: "A complete guide to SSR with React.",
246
538
  type: "article",
247
539
  url: "https://myblog.com/blog/ssr-guide",
248
- images: [{
249
- url: "https://myblog.com/images/ssr-guide.jpg",
250
- width: 1200, height: 630,
251
- alt: "SSR Guide Cover",
252
- }],
253
- },
254
- twitter: {
255
- title: "How to Build an SSR App",
256
- creator: "@authorhandle",
257
- image: "https://myblog.com/images/ssr-guide.jpg",
540
+ images: [{ url: "https://myblog.com/images/ssr-guide.jpg", width: 1200, height: 630, alt: "SSR Guide" }],
258
541
  },
542
+ twitter: { title: "How to Build an SSR App", creator: "@authorhandle", image: "https://myblog.com/images/ssr-guide.jpg" },
259
543
  });
260
544
 
261
- // ── Structured Data ───────────────────────────────────────
262
545
  const article = createArticleSchema({
263
546
  headline: "How to Build an SSR App",
264
547
  url: "https://myblog.com/blog/ssr-guide",
265
- description: "A complete guide to SSR with React.",
266
548
  datePublished: "2025-06-15",
267
549
  dateModified: "2025-07-01",
268
- author: [
269
- { name: "Jane Doe", url: "https://myblog.com/authors/jane" },
270
- { name: "John Smith" },
271
- ],
550
+ author: [{ name: "Jane Doe", url: "https://myblog.com/authors/jane" }],
272
551
  publisher: { name: "My Blog", logo: "https://myblog.com/logo.png" },
273
552
  images: ["https://myblog.com/images/ssr-guide.jpg"],
274
553
  section: "Technology",
@@ -281,12 +560,10 @@ export function BlogPostPage() {
281
560
  { name: "How to Build an SSR App", url: "https://myblog.com/blog/ssr-guide" },
282
561
  ]);
283
562
 
284
- // ── Render — no <html> or <head> tags! ────────────────────
285
563
  return (
286
564
  <Document seo={seo} schemas={[article, breadcrumbs]}>
287
565
  <article>
288
566
  <h1>How to Build an SSR App</h1>
289
- <p>Your article content here...</p>
290
567
  </article>
291
568
  </Document>
292
569
  );
@@ -295,36 +572,6 @@ export function BlogPostPage() {
295
572
 
296
573
  <br />
297
574
 
298
- ### Generated HTML Output
299
-
300
- ```html
301
- <head>
302
- <!-- Basic -->
303
- <title>How to Build an SSR App | My Blog</title>
304
- <meta name="description" content="A complete guide to building server-rendered React apps..." />
305
- <link rel="canonical" href="https://myblog.com/blog/ssr-guide" />
306
-
307
- <!-- Open Graph -->
308
- <meta property="og:title" content="How to Build an SSR App" />
309
- <meta property="og:description" content="A complete guide to SSR with React." />
310
- <meta property="og:type" content="article" />
311
- <meta property="og:url" content="https://myblog.com/blog/ssr-guide" />
312
- <meta property="og:site_name" content="My Blog" />
313
- <meta property="og:image" content="https://myblog.com/images/ssr-guide.jpg" />
314
-
315
- <!-- Twitter Card -->
316
- <meta name="twitter:card" content="summary_large_image" />
317
- <meta name="twitter:site" content="@myblog" />
318
- <meta name="twitter:title" content="How to Build an SSR App" />
319
-
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>
323
- </head>
324
- ```
325
-
326
- <br />
327
-
328
575
  ---
329
576
 
330
577
  <br />
@@ -332,59 +579,38 @@ export function BlogPostPage() {
332
579
  ### E-Commerce Product Page
333
580
 
334
581
  ```tsx
335
- import {
336
- mergeSEOConfig, buildCanonicalUrl,
337
- createProductSchema,
338
- } from "react-ssr-seo-toolkit";
339
- import { siteConfig, SITE_URL } from "../config/seo";
340
- import { Document } from "../components/Document";
582
+ import { mergeSEOConfig, buildCanonicalUrl, createProductSchema } from "react-ssr-seo-toolkit";
341
583
 
342
584
  function ProductPage() {
343
- const product = {
344
- name: "Ergonomic Mechanical Keyboard",
345
- description: "Premium split keyboard with Cherry MX Brown switches.",
346
- price: 189.99,
347
- image: "https://acmestore.com/images/keyboard.jpg",
348
- brand: "Acme Peripherals",
349
- sku: "ACME-KB-001",
350
- inStock: true,
351
- rating: 4.7,
352
- reviewCount: 342,
353
- };
354
-
355
585
  const url = buildCanonicalUrl(SITE_URL, "/products/ergonomic-keyboard");
356
586
 
357
587
  const seo = mergeSEOConfig(siteConfig, {
358
- title: product.name,
359
- description: product.description,
588
+ title: "Ergonomic Mechanical Keyboard",
589
+ description: "Premium split keyboard with Cherry MX Brown switches.",
360
590
  canonical: url,
361
591
  openGraph: {
362
- title: product.name,
363
- description: product.description,
364
592
  type: "product",
365
593
  url,
366
- images: [{ url: product.image, width: 800, height: 800, alt: product.name }],
594
+ images: [{ url: "https://acmestore.com/images/keyboard.jpg", width: 800, height: 800, alt: "Keyboard" }],
367
595
  },
368
596
  });
369
597
 
370
598
  const schema = createProductSchema({
371
- name: product.name,
599
+ name: "Ergonomic Mechanical Keyboard",
372
600
  url,
373
- description: product.description,
374
- price: product.price,
601
+ price: 189.99,
375
602
  priceCurrency: "USD",
376
- availability: product.inStock ? "InStock" : "OutOfStock",
377
- brand: product.brand,
378
- sku: product.sku,
379
- images: [product.image],
380
- ratingValue: product.rating,
381
- reviewCount: product.reviewCount,
603
+ availability: "InStock",
604
+ brand: "Acme Peripherals",
605
+ sku: "ACME-KB-001",
606
+ images: ["https://acmestore.com/images/keyboard.jpg"],
607
+ ratingValue: 4.7,
608
+ reviewCount: 342,
382
609
  });
383
610
 
384
611
  return (
385
612
  <Document seo={seo} schemas={[schema]}>
386
- <h1>{product.name}</h1>
387
- <p>${product.price}</p>
613
+ <h1>Ergonomic Mechanical Keyboard</h1>
388
614
  </Document>
389
615
  );
390
616
  }
@@ -396,40 +622,62 @@ function ProductPage() {
396
622
 
397
623
  <br />
398
624
 
399
- ### FAQ Page
625
+ ### Job Posting Schema
400
626
 
401
627
  ```tsx
402
- import {
403
- mergeSEOConfig, buildCanonicalUrl, createFAQSchema,
404
- } from "react-ssr-seo-toolkit";
405
- import { siteConfig, SITE_URL } from "../config/seo";
406
- import { Document } from "../components/Document";
628
+ import { createJobPostingSchema } from "react-ssr-seo-toolkit";
629
+
630
+ const schema = createJobPostingSchema({
631
+ title: "Senior React Engineer",
632
+ description: "Build and maintain our SSR platform...",
633
+ datePosted: "2026-05-01",
634
+ validThrough: "2026-06-30",
635
+ employmentType: "FULL_TIME",
636
+ hiringOrganization: {
637
+ name: "Trustix UK",
638
+ sameAs: "https://trustix.uk",
639
+ logo: "https://trustix.uk/logo.png",
640
+ },
641
+ jobLocation: { addressLocality: "London", addressCountry: "GB" },
642
+ remote: true,
643
+ baseSalary: {
644
+ currency: "GBP",
645
+ value: { minValue: 70000, maxValue: 95000 },
646
+ unitText: "YEAR",
647
+ },
648
+ });
649
+ ```
407
650
 
408
- function FAQPage() {
409
- const faqs = [
410
- { question: "What payment methods do you accept?", answer: "Visa, MasterCard, PayPal, Apple Pay." },
411
- { question: "How long does shipping take?", answer: "Standard: 3-5 business days." },
412
- { question: "What is your return policy?", answer: "30-day money-back guarantee." },
413
- ];
651
+ <br />
414
652
 
415
- const seo = mergeSEOConfig(siteConfig, {
416
- title: "FAQ",
417
- description: "Frequently asked questions about our products and services.",
418
- canonical: buildCanonicalUrl(SITE_URL, "/faq"),
419
- });
653
+ ---
420
654
 
421
- return (
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>
431
- );
432
- }
655
+ <br />
656
+
657
+ ### Recipe Schema
658
+
659
+ ```tsx
660
+ import { createRecipeSchema } from "react-ssr-seo-toolkit";
661
+
662
+ const schema = createRecipeSchema({
663
+ name: "Chicken Biryani",
664
+ description: "Authentic Dhaka-style biryani with aromatic spices.",
665
+ images: ["https://food.com/biryani.jpg"],
666
+ author: { name: "Chef Rahman" },
667
+ prepTime: "PT30M",
668
+ cookTime: "PT1H",
669
+ totalTime: "PT1H30M",
670
+ recipeYield: "6 servings",
671
+ recipeCategory: "Main Course",
672
+ recipeCuisine: "Bengali",
673
+ recipeIngredient: ["1kg chicken", "500g basmati rice", "onions"],
674
+ recipeInstructions: [
675
+ { name: "Marinate", text: "Marinate chicken with spices for 30 minutes." },
676
+ { name: "Cook Rice", text: "Parboil basmati rice until 70% done." },
677
+ ],
678
+ ratingValue: 4.9,
679
+ reviewCount: 128,
680
+ });
433
681
  ```
434
682
 
435
683
  <br />
@@ -438,59 +686,49 @@ function FAQPage() {
438
686
 
439
687
  <br />
440
688
 
441
- ### Homepage (Organization + Website Schema)
689
+ ### Person Schema
442
690
 
443
691
  ```tsx
444
- import {
445
- mergeSEOConfig,
446
- createOrganizationSchema, createWebsiteSchema,
447
- } from "react-ssr-seo-toolkit";
448
- import { siteConfig } from "../config/seo";
449
- import { Document } from "../components/Document";
692
+ import { createPersonSchema } from "react-ssr-seo-toolkit";
693
+
694
+ const schema = createPersonSchema({
695
+ name: "Tonmoy Khan",
696
+ url: "https://tonmoykhan.site",
697
+ jobTitle: "Software Engineer",
698
+ image: "https://tonmoykhan.site/photo.jpg",
699
+ sameAs: [
700
+ "https://github.com/Tonmoy01",
701
+ "https://linkedin.com/in/tonmoy",
702
+ ],
703
+ worksFor: { name: "Trustix UK", url: "https://trustix.uk" },
704
+ });
705
+ ```
450
706
 
451
- function HomePage() {
452
- const seo = mergeSEOConfig(siteConfig, {
453
- title: "Home",
454
- canonical: "https://acme.com",
455
- openGraph: {
456
- title: "Acme — Building the future",
457
- url: "https://acme.com",
458
- images: [{ url: "https://acme.com/og-home.jpg", width: 1200, height: 630, alt: "Acme" }],
459
- },
460
- });
707
+ <br />
461
708
 
462
- const org = createOrganizationSchema({
463
- name: "Acme Inc",
464
- url: "https://acme.com",
465
- logo: "https://acme.com/logo.png",
466
- description: "Leading provider of quality products.",
467
- sameAs: [
468
- "https://twitter.com/acme",
469
- "https://linkedin.com/company/acme",
470
- "https://facebook.com/acme",
471
- ],
472
- contactPoint: {
473
- telephone: "+1-800-555-0199",
474
- contactType: "customer service",
475
- email: "support@acme.com",
476
- areaServed: "US",
477
- availableLanguage: ["English", "Spanish"],
478
- },
479
- });
709
+ ---
480
710
 
481
- const site = createWebsiteSchema({
482
- name: "Acme Inc",
483
- url: "https://acme.com",
484
- description: "Leading provider of quality products.",
485
- searchUrl: "https://acme.com/search", // enables Google sitelinks searchbox
486
- });
711
+ <br />
487
712
 
488
- return (
489
- <Document seo={seo} schemas={[org, site]}>
490
- <h1>Welcome to Acme</h1>
491
- </Document>
492
- );
493
- }
713
+ ### Event Page (Sports Match)
714
+
715
+ ```tsx
716
+ import { createEventSchema } from "react-ssr-seo-toolkit";
717
+
718
+ // @type auto-switches to "SportsEvent" when sport/homeTeam/awayTeam is provided
719
+ const schema = createEventSchema({
720
+ name: "El Clásico",
721
+ startDate: "2026-04-20T21:00:00+02:00",
722
+ sport: "Soccer",
723
+ homeTeam: { name: "FC Barcelona", url: "https://fcbarcelona.com" },
724
+ awayTeam: { name: "Real Madrid CF", url: "https://realmadrid.com" },
725
+ location: {
726
+ name: "Camp Nou",
727
+ address: { addressLocality: "Barcelona", addressCountry: "ES" },
728
+ },
729
+ eventStatus: "EventScheduled",
730
+ eventAttendanceMode: "OfflineEventAttendanceMode",
731
+ });
494
732
  ```
495
733
 
496
734
  <br />
@@ -512,11 +750,6 @@ const seo = mergeSEOConfig(siteConfig, {
512
750
  { hreflang: "x-default", href: "https://mysite.com/products" },
513
751
  ],
514
752
  });
515
-
516
- // Generates:
517
- // <link rel="alternate" hreflang="en" href="https://mysite.com/en/products" />
518
- // <link rel="alternate" hreflang="es" href="https://mysite.com/es/products" />
519
- // ...
520
753
  ```
521
754
 
522
755
  <br />
@@ -525,57 +758,20 @@ const seo = mergeSEOConfig(siteConfig, {
525
758
 
526
759
  <br />
527
760
 
528
- ### No-Index Pages (Admin, Login, Drafts)
761
+ ### No-Index Pages
529
762
 
530
763
  ```tsx
531
764
  import { mergeSEOConfig, noIndex, noIndexNoFollow } from "react-ssr-seo-toolkit";
532
765
 
533
- // Login page: don't index, but follow links
534
766
  const loginSeo = mergeSEOConfig(siteConfig, {
535
767
  title: "Login",
536
768
  robots: noIndex(), // "noindex, follow"
537
769
  });
538
770
 
539
- // Admin page: don't index, don't follow
540
771
  const adminSeo = mergeSEOConfig(siteConfig, {
541
772
  title: "Admin Dashboard",
542
773
  robots: noIndexNoFollow(), // "noindex, nofollow"
543
774
  });
544
-
545
- // Fine-grained control
546
- const archiveSeo = mergeSEOConfig(siteConfig, {
547
- title: "Archive",
548
- robots: {
549
- index: true,
550
- follow: true,
551
- noarchive: true,
552
- nosnippet: true,
553
- maxSnippet: 50,
554
- maxImagePreview: "standard",
555
- },
556
- });
557
- ```
558
-
559
- <br />
560
-
561
- ---
562
-
563
- <br />
564
-
565
- ### Combine Multiple Schemas
566
-
567
- ```tsx
568
- import { composeSchemas, createOrganizationSchema, createWebsiteSchema, JsonLd } from "react-ssr-seo-toolkit";
569
-
570
- // Merge into a single JSON-LD block with @graph array
571
- const combined = composeSchemas(
572
- createOrganizationSchema({ name: "Acme", url: "https://acme.com" }),
573
- createWebsiteSchema({ name: "Acme", url: "https://acme.com" }),
574
- );
575
-
576
- <JsonLd data={combined} />
577
-
578
- // Output: single <script> tag with {"@context":"https://schema.org","@graph":[...]}
579
775
  ```
580
776
 
581
777
  <br />
@@ -590,131 +786,52 @@ const combined = composeSchemas(
590
786
 
591
787
  ```tsx
592
788
  // app/blog/[slug]/page.tsx
593
- import {
594
- buildTitle, buildDescription, buildCanonicalUrl,
595
- createArticleSchema, safeJsonLdSerialize,
596
- } from "react-ssr-seo-toolkit";
789
+ import { toNextMetadata } from "react-ssr-seo-toolkit/adapters/nextjs";
790
+ import { createArticleSchema, safeJsonLdSerialize, buildCanonicalUrl } from "react-ssr-seo-toolkit";
791
+ import type { Metadata } from "next";
597
792
 
598
- export async function generateMetadata({ params }) {
793
+ export async function generateMetadata({ params }): Promise<Metadata> {
599
794
  const post = await getPost(params.slug);
600
-
601
- return {
602
- title: buildTitle(post.title, "%s | My Blog"),
603
- description: buildDescription(post.excerpt, 160),
604
- alternates: {
605
- canonical: buildCanonicalUrl("https://myblog.com", `/blog/${params.slug}`),
606
- },
795
+ return toNextMetadata({
796
+ title: post.title,
797
+ titleTemplate: "%s | My Blog",
798
+ description: post.excerpt,
799
+ canonical: buildCanonicalUrl("https://myblog.com", `/blog/${params.slug}`),
607
800
  openGraph: {
608
801
  title: post.title,
609
802
  type: "article",
610
- images: [{ url: post.image, width: 1200, height: 630 }],
803
+ images: [{ url: post.image, width: 1200, height: 630, alt: post.title }],
611
804
  },
612
- };
613
- }
614
-
615
- export default function BlogPost({ params }) {
616
- const post = getPost(params.slug);
617
-
618
- const schema = createArticleSchema({
619
- headline: post.title,
620
- url: `https://myblog.com/blog/${params.slug}`,
621
- datePublished: post.date,
622
- author: { name: post.author },
805
+ twitter: { card: "summary_large_image" },
623
806
  });
624
-
625
- return (
626
- <>
627
- <script
628
- type="application/ld+json"
629
- dangerouslySetInnerHTML={{ __html: safeJsonLdSerialize(schema) }}
630
- />
631
- <article>
632
- <h1>{post.title}</h1>
633
- <p>{post.content}</p>
634
- </article>
635
- </>
636
- );
637
807
  }
638
808
  ```
639
809
 
640
810
  <br />
641
811
 
642
- ### Next.js Pages Router
812
+ ### React Router 7
643
813
 
644
814
  ```tsx
645
- // pages/about.tsx — no <html> tags, Next.js handles that
646
- import Head from "next/head";
647
- import { SEOHead, mergeSEOConfig } from "react-ssr-seo-toolkit";
648
- import { siteConfig } from "../config/seo";
649
-
650
- export default function AboutPage() {
651
- const seo = mergeSEOConfig(siteConfig, {
652
- title: "About Us",
653
- description: "Learn about our mission.",
654
- canonical: "https://mysite.com/about",
655
- });
656
-
657
- return (
658
- <>
659
- <Head>
660
- <SEOHead {...seo} />
661
- </Head>
662
- <main>
663
- <h1>About Us</h1>
664
- </main>
665
- </>
666
- );
667
- }
668
- ```
669
-
670
- <br />
671
-
672
- ### React Router 7 SSR
673
-
674
- ```tsx
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";
678
-
679
- export default function Root() {
680
- const matches = useMatches();
681
- const seo = matches.at(-1)?.data?.seo;
682
-
683
- return (
684
- <html lang="en">
685
- <head>
686
- <meta charSet="utf-8" />
687
- <meta name="viewport" content="width=device-width, initial-scale=1" />
688
- {seo && <SEOHead {...seo} />}
689
- </head>
690
- <body>
691
- <Outlet />
692
- </body>
693
- </html>
694
- );
695
- }
696
- ```
697
-
698
- ```tsx
699
- // app/routes/about.tsx — page just provides SEO data + content
815
+ // app/routes/about.tsx
816
+ import { toRouterMeta } from "react-ssr-seo-toolkit/adapters/react-router";
700
817
  import { mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
701
818
  import { siteConfig, SITE_URL } from "../config/seo";
819
+ import type { Route } from "./+types/about";
702
820
 
703
- export function loader() {
704
- return {
705
- seo: mergeSEOConfig(siteConfig, {
706
- title: "About",
821
+ export function meta({}: Route.MetaArgs) {
822
+ return toRouterMeta(
823
+ mergeSEOConfig(siteConfig, {
824
+ title: "About Us",
825
+ description: "Learn about our company.",
707
826
  canonical: buildCanonicalUrl(SITE_URL, "/about"),
708
- }),
709
- };
827
+ openGraph: { type: "website", title: "About Us" },
828
+ twitter: { card: "summary_large_image" },
829
+ })
830
+ );
710
831
  }
711
832
 
712
833
  export default function AboutPage() {
713
- return (
714
- <main>
715
- <h1>About Us</h1>
716
- </main>
717
- );
834
+ return <main><h1>About Us</h1></main>;
718
835
  }
719
836
  ```
720
837
 
@@ -723,54 +840,26 @@ export default function AboutPage() {
723
840
  ### Express + React SSR
724
841
 
725
842
  ```tsx
726
- // server.tsx renders page components that include Document internally
727
- import express from "express";
728
- import { renderToString } from "react-dom/server";
729
- import { HomePage } from "./pages/HomePage";
730
- import { ProductPage } from "./pages/ProductPage";
731
-
732
- const app = express();
733
-
734
- app.get("/", (req, res) => {
735
- const html = renderToString(<HomePage />);
736
- res.send(`<!DOCTYPE html>${html}`);
843
+ app.get("/sitemap.xml", (req, res) => {
844
+ const { generateSitemap } = require("react-ssr-seo-toolkit/sitemap");
845
+ res.type("application/xml").send(
846
+ generateSitemap({ baseUrl: "https://mysite.com", routes: appRoutes, exclude: ["/admin/*"] })
847
+ );
737
848
  });
738
849
 
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}`);
850
+ app.get("/robots.txt", (req, res) => {
851
+ const { generateRobots } = require("react-ssr-seo-toolkit/sitemap");
852
+ res.type("text/plain").send(
853
+ generateRobots({ rules: { disallow: ["/admin"] }, sitemap: "https://mysite.com/sitemap.xml" })
854
+ );
743
855
  });
744
856
 
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 }) {
755
- const seo = mergeSEOConfig(siteConfig, {
756
- title: product.name,
757
- description: product.description,
758
- canonical: product.url,
759
- });
760
-
761
- const schema = createProductSchema({
762
- name: product.name,
763
- url: product.url,
764
- price: product.price,
765
- });
766
-
767
- return (
768
- <Document seo={seo} schemas={[schema]}>
769
- <h1>{product.name}</h1>
770
- <p>${product.price}</p>
771
- </Document>
857
+ app.get("/og/:title.svg", (req, res) => {
858
+ const { createOGImageSVG } = require("react-ssr-seo-toolkit/og");
859
+ res.type("image/svg+xml").send(
860
+ createOGImageSVG({ title: decodeURIComponent(req.params.title), brand: "MySite" })
772
861
  );
773
- }
862
+ });
774
863
  ```
775
864
 
776
865
  <br />
@@ -786,7 +875,7 @@ export function ProductPage({ product }) {
786
875
  | Function | What It Does |
787
876
  |---|---|
788
877
  | `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. |
878
+ | `mergeSEOConfig(base, override)` | Deep-merge site config with page-level overrides. |
790
879
  | `normalizeSEOConfig(config)` | Trim strings, normalize URLs, clean up a config object. |
791
880
 
792
881
  <br />
@@ -796,42 +885,101 @@ export function ProductPage({ product }) {
796
885
  | Function | Example | Result |
797
886
  |---|---|---|
798
887
  | `buildTitle(title, template)` | `buildTitle("About", "%s \| MySite")` | `"About \| MySite"` |
799
- | `buildDescription(desc, maxLen)` | `buildDescription("Long text...", 160)` | Truncated at 160 chars |
800
888
  | `buildCanonicalUrl(base, path)` | `buildCanonicalUrl("https://x.com", "/about")` | `"https://x.com/about"` |
801
889
  | `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: "..." }]` |
890
+ | `noIndex()` | | `{ index: false, follow: true }` |
891
+ | `noIndexNoFollow()` | | `{ index: false, follow: false }` |
807
892
 
808
893
  <br />
809
894
 
810
895
  ### JSON-LD Schema Generators
811
896
 
812
- All return a plain object with `@context: "https://schema.org"` and `@type` set.
897
+ All return `{ "@context": "https://schema.org", "@type": "...", ... }`.
813
898
 
814
899
  | Function | Schema Type | Use Case |
815
900
  |---|---|---|
816
901
  | `createOrganizationSchema(input)` | Organization | Company info, logo, social links, contact |
817
902
  | `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 |
903
+ | `createArticleSchema(input)` | Article | Blog posts, news articles |
904
+ | `createProductSchema(input)` | Product | E-commerce: price, brand, SKU, ratings |
820
905
  | `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 |
906
+ | `createFAQSchema(items)` | FAQPage | Question + answer pairs |
907
+ | `createEventSchema(input)` | Event / SportsEvent | Concerts, conferences, sports auto-switches to `SportsEvent` when `sport`/`homeTeam`/`awayTeam` is set |
908
+ | `createPersonSchema(input)` | Person | Author bios, team pages |
909
+ | `createRecipeSchema(input)` | Recipe | Food blogs, cooking sites |
910
+ | `createJobPostingSchema(input)` | JobPosting | Career pages, job boards |
911
+ | `composeSchemas(...schemas)` | @graph | Combine multiple schemas into one `<script>` |
823
912
 
824
913
  <br />
825
914
 
826
- ### Utilities
915
+ ### Sitemap & Robots — `react-ssr-seo-toolkit/sitemap`
827
916
 
828
917
  | Function | What It Does |
829
918
  |---|---|
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) |
919
+ | `generateSitemap(options)` | Generates a valid `sitemap.xml` string from a list of routes |
920
+ | `generateRobots(options)` | Generates a `robots.txt` string with user-agent rules and sitemap links |
921
+ | `autoBreadcrumb(path, options?)` | Auto-generates `BreadcrumbItem[]` from a URL path like `/ticket/liverpool-vs-arsenal` |
922
+
923
+ **`generateSitemap` options:**
924
+
925
+ | Option | Type | Description |
926
+ |---|---|---|
927
+ | `routes` | `string[] \| SitemapRoute[]` | Route paths or route objects with `lastmod`, `changefreq`, `priority` |
928
+ | `baseUrl` | `string` | Base URL (e.g. `"https://mysite.com"`) |
929
+ | `exclude` | `string[]` | Glob patterns to exclude (supports `*` wildcard and `/*` suffix) |
930
+ | `defaultChangefreq` | `ChangeFreq` | Default change frequency for all routes |
931
+ | `defaultPriority` | `number` | Default priority (0.0–1.0) for all routes |
932
+
933
+ <br />
934
+
935
+ ### SEO Validation & Scoring — `react-ssr-seo-toolkit/validation`
936
+
937
+ | Function | What It Does |
938
+ |---|---|
939
+ | `validateSEO(routes)` | Checks a list of routes for missing/invalid SEO fields, returns `SEOValidationIssue[]` |
940
+ | `printValidationReport(issues)` | Formats issues into a human-readable string with icons |
941
+ | `getSEOScore(config, pageName?)` | Scores a single page's SEO config out of 100 |
942
+ | `formatSEOScore(result)` | Formats the score result as a pretty-printed string |
943
+
944
+ **Scoring breakdown (100 points total):**
945
+
946
+ | Check | Points |
947
+ |---|---|
948
+ | Title (≤60 chars) | 20 |
949
+ | Description (≤160 chars) | 15 |
950
+ | Canonical URL | 10 |
951
+ | og:image | 15 |
952
+ | og:title | 5 |
953
+ | og:description | 5 |
954
+ | Twitter Card | 10 |
955
+ | Structured Data (JSON-LD) | 10 |
956
+ | Robots Directives | 5 |
957
+ | Hreflang | 5 |
958
+
959
+ <br />
960
+
961
+ ### OG Image Generation — `react-ssr-seo-toolkit/og`
962
+
963
+ | Function | What It Does |
964
+ |---|---|
965
+ | `createOGImageSVG(options)` | Returns a 1200×630 SVG string — serve as `image/svg+xml` or convert to PNG with `sharp` |
966
+
967
+ **Options:**
968
+
969
+ | Option | Type | Description |
970
+ |---|---|---|
971
+ | `title` | `string` | Main headline |
972
+ | `description` | `string?` | Subtitle / description |
973
+ | `template` | `"default" \| "article" \| "sports-event"` | Visual template |
974
+ | `brand` | `string?` | Brand name shown in corner |
975
+ | `backgroundColor` | `string?` | Override background color |
976
+ | `textColor` | `string?` | Override text color |
977
+ | `accentColor` | `string?` | Override accent / highlight color |
978
+ | `category` | `string?` | Category tag (article template) |
979
+ | `author` | `string?` | Author name (article template) |
980
+ | `dateString` | `string?` | Display date (article template) |
981
+ | `homeTeam` / `awayTeam` | `string?` | Team names (sports-event template) |
982
+ | `eventDate` | `string?` | Event date display (sports-event template) |
835
983
 
836
984
  <br />
837
985
 
@@ -839,42 +987,17 @@ All return a plain object with `@context: "https://schema.org"` and `@type` set.
839
987
 
840
988
  #### `<SEOHead>`
841
989
 
842
- Renders all SEO tags as React elements. Place inside `<head>`.
990
+ Renders all SEO tags inside `<head>`. Accepts the full `SEOConfig` plus an optional `nonce` for CSP.
843
991
 
844
992
  ```tsx
845
993
  <SEOHead
846
994
  title="My Page"
847
995
  titleTemplate="%s | MySite"
848
- description="Page description here."
996
+ description="Page description."
849
997
  canonical="https://mysite.com/page"
850
998
  robots={{ index: true, follow: true }}
851
- openGraph={{
852
- title: "My Page",
853
- description: "For social sharing.",
854
- type: "website",
855
- url: "https://mysite.com/page",
856
- siteName: "MySite",
857
- locale: "en_US",
858
- images: [{ url: "https://mysite.com/og.jpg", width: 1200, height: 630, alt: "Preview" }],
859
- }}
860
- twitter={{
861
- card: "summary_large_image",
862
- site: "@mysite",
863
- creator: "@author",
864
- title: "My Page",
865
- image: "https://mysite.com/twitter.jpg",
866
- }}
867
- alternates={[
868
- { hreflang: "en", href: "https://mysite.com/en/page" },
869
- { hreflang: "es", href: "https://mysite.com/es/page" },
870
- ]}
871
- additionalMetaTags={[
872
- { name: "author", content: "Jane Doe" },
873
- ]}
874
- additionalLinkTags={[
875
- { rel: "icon", href: "/favicon.ico" },
876
- ]}
877
- jsonLd={createArticleSchema({ headline: "...", url: "..." })}
999
+ openGraph={{ type: "website", images: [{ url: "...", width: 1200, height: 630 }] }}
1000
+ twitter={{ card: "summary_large_image", site: "@mysite" }}
878
1001
  />
879
1002
  ```
880
1003
 
@@ -886,6 +1009,53 @@ Standalone JSON-LD `<script>` tag renderer.
886
1009
  <JsonLd data={createProductSchema({ name: "Widget", url: "...", price: 29.99 })} />
887
1010
  ```
888
1011
 
1012
+ #### `<SEOPreview>` — `react-ssr-seo-toolkit/components`
1013
+
1014
+ Renders a visual social card preview. Useful in dev mode or a CMS preview panel.
1015
+
1016
+ ```tsx
1017
+ <SEOPreview config={seoConfig} platform="twitter" /> // Twitter card
1018
+ <SEOPreview config={seoConfig} platform="facebook" /> // Facebook / OG
1019
+ <SEOPreview config={seoConfig} platform="linkedin" /> // LinkedIn post
1020
+ <SEOPreview config={seoConfig} platform="google" /> // Google search result
1021
+ ```
1022
+
1023
+ | Prop | Type | Default |
1024
+ |---|---|---|
1025
+ | `config` | `SEOConfig` | required |
1026
+ | `platform` | `"twitter" \| "facebook" \| "linkedin" \| "google"` | `"twitter"` |
1027
+ | `style` | `React.CSSProperties` | — |
1028
+
1029
+ <br />
1030
+
1031
+ ### Framework Adapters
1032
+
1033
+ #### `react-ssr-seo-toolkit/adapters/nextjs`
1034
+
1035
+ | Function | What It Does |
1036
+ |---|---|
1037
+ | `toNextMetadata(config)` | Converts `SEOConfig` → Next.js App Router `Metadata` object |
1038
+ | `buildNextTitle(config)` | Returns the resolved title string |
1039
+
1040
+ #### `react-ssr-seo-toolkit/adapters/react-router`
1041
+
1042
+ | Function | What It Does |
1043
+ |---|---|
1044
+ | `toRouterMeta(config)` | Converts `SEOConfig` → `MetaDescriptor[]` for a route's `meta()` export |
1045
+ | `toRouterLinks(config)` | Converts canonical + hreflang → `LinkDescriptor[]` for a route's `links()` export |
1046
+
1047
+ <br />
1048
+
1049
+ ### Utilities
1050
+
1051
+ | Function | What It Does |
1052
+ |---|---|
1053
+ | `safeJsonLdSerialize(data)` | Serialize JSON-LD safely — escapes `<`, `>`, `&` to prevent XSS |
1054
+ | `normalizeUrl(url)` | Trim whitespace, remove trailing slashes |
1055
+ | `buildFullUrl(base, path?)` | Combine base URL with path |
1056
+ | `omitEmpty(obj)` | Remove keys with `undefined`, `null`, or `""` values |
1057
+ | `deepMerge(base, override)` | Deep-merge two objects (arrays replaced, not concatenated) |
1058
+
889
1059
  <br />
890
1060
 
891
1061
  ### TypeScript Types
@@ -893,22 +1063,28 @@ Standalone JSON-LD `<script>` tag renderer.
893
1063
  ```tsx
894
1064
  import type {
895
1065
  SEOConfig,
896
- OpenGraphConfig,
897
- OpenGraphImage,
898
- OpenGraphType, // "website" | "article" | "product" | "profile" | ...
899
- TwitterConfig,
900
- TwitterCardType, // "summary" | "summary_large_image" | "app" | "player"
1066
+ OpenGraphConfig, OpenGraphImage, OpenGraphType,
1067
+ TwitterConfig, TwitterCardType,
901
1068
  RobotsConfig,
902
1069
  AlternateLink,
903
- JSONLDBase,
904
- BreadcrumbItem,
905
- OrganizationSchemaInput,
906
- WebsiteSchemaInput,
907
- ArticleSchemaInput,
908
- ProductSchemaInput,
1070
+ JSONLDBase, BreadcrumbItem,
1071
+ OrganizationSchemaInput, WebsiteSchemaInput,
1072
+ ArticleSchemaInput, ProductSchemaInput,
909
1073
  FAQItem,
910
- SEOHeadProps,
911
- JsonLdProps,
1074
+ EventSchemaInput, EventStatus, EventAttendanceMode,
1075
+ PersonSchemaInput,
1076
+ RecipeSchemaInput, RecipeInstruction,
1077
+ JobPostingSchemaInput,
1078
+ // Sitemap
1079
+ SitemapRoute, GenerateSitemapOptions, ChangeFreq,
1080
+ RobotsRule, GenerateRobotsOptions,
1081
+ // Validation
1082
+ SEOValidationIssue, RouteWithSEO,
1083
+ SEOScoreResult, SEOScoreCheck,
1084
+ // OG Image
1085
+ OGImageOptions, OGImageTemplate,
1086
+ // Components
1087
+ SEOHeadProps, JsonLdProps, SEOPreviewProps,
912
1088
  } from "react-ssr-seo-toolkit";
913
1089
  ```
914
1090
 
@@ -918,9 +1094,28 @@ import type {
918
1094
 
919
1095
  <br />
920
1096
 
921
- ## Live Demo
1097
+ ## Entry Points
1098
+
1099
+ The package ships separate entry points so you only import what you need:
1100
+
1101
+ | Import | Contents |
1102
+ |---|---|
1103
+ | `react-ssr-seo-toolkit` | Everything — core, schema, utils, components |
1104
+ | `react-ssr-seo-toolkit/schema` | Schema generators only (no React) |
1105
+ | `react-ssr-seo-toolkit/sitemap` | `generateSitemap`, `generateRobots`, `autoBreadcrumb` |
1106
+ | `react-ssr-seo-toolkit/validation` | `validateSEO`, `getSEOScore`, `formatSEOScore` |
1107
+ | `react-ssr-seo-toolkit/og` | `createOGImageSVG` |
1108
+ | `react-ssr-seo-toolkit/components` | React components — `SEOHead`, `JsonLd`, `SEOPreview` |
1109
+ | `react-ssr-seo-toolkit/adapters/nextjs` | Next.js adapter |
1110
+ | `react-ssr-seo-toolkit/adapters/react-router` | React Router 7 adapter |
1111
+
1112
+ <br />
1113
+
1114
+ ---
922
1115
 
923
- The repo includes a **working Express SSR demo** with every feature:
1116
+ <br />
1117
+
1118
+ ## Live Demo
924
1119
 
925
1120
  ```bash
926
1121
  git clone https://github.com/Tonmoy01/react-ssr-seo-toolkit.git
@@ -929,19 +1124,7 @@ npm install
929
1124
  npm run demo
930
1125
  ```
931
1126
 
932
- Then visit [http://localhost:3000](http://localhost:3000):
933
-
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 |
943
-
944
- > **Tip:** Right-click any page and **View Page Source** to see all SEO tags in the raw HTML.
1127
+ Then visit [http://localhost:3000](http://localhost:3000).
945
1128
 
946
1129
  <br />
947
1130
 
@@ -956,9 +1139,7 @@ npm install # install dependencies
956
1139
  npm run build # build the library
957
1140
  npm run dev # watch mode (auto-rebuild)
958
1141
  npm test # run tests
959
- npm run test:watch # tests in watch mode
960
1142
  npm run lint # type check
961
- npm run clean # clean build output
962
1143
  npm run demo # run demo server
963
1144
  ```
964
1145
 
@@ -972,24 +1153,16 @@ npm run demo # run demo server
972
1153
 
973
1154
  ### "Cannot find module 'react-ssr-seo-toolkit'"
974
1155
 
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.
1156
+ Ensure the package is installed and your bundler supports the `exports` field in `package.json`.
976
1157
 
977
1158
  ### Hydration mismatch warnings
978
1159
 
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.
1160
+ `<SEOHead>` produces deterministic output. Ensure the same config object is used on both server and client. Avoid using `Date.now()` or random values in your SEO config.
980
1161
 
981
1162
  ### JSON-LD not appearing in page source
982
1163
 
983
1164
  Make sure `<JsonLd>` is inside `<head>` and rendered during SSR — not in a client-only `useEffect`.
984
1165
 
985
- ### TypeScript errors
986
-
987
- All types are exported. Import them directly:
988
-
989
- ```tsx
990
- import type { SEOConfig, OpenGraphConfig } from "react-ssr-seo-toolkit";
991
- ```
992
-
993
1166
  <br />
994
1167
 
995
1168
  ---