react-ssr-seo-toolkit 1.1.0 → 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 (70) hide show
  1. package/README.md +592 -512
  2. package/dist/adapters/nextjs.d.cts +1 -1
  3. package/dist/adapters/nextjs.d.ts +1 -1
  4. package/dist/adapters/react-router.d.cts +1 -1
  5. package/dist/adapters/react-router.d.ts +1 -1
  6. package/dist/chunk-3UCLUFOI.js +5 -0
  7. package/dist/chunk-3UCLUFOI.js.map +1 -0
  8. package/dist/chunk-A62THBIS.cjs +3 -0
  9. package/dist/chunk-A62THBIS.cjs.map +1 -0
  10. package/dist/chunk-GNLXGAS6.js +3 -0
  11. package/dist/chunk-GNLXGAS6.js.map +1 -0
  12. package/dist/chunk-LEOORZQY.cjs +5 -0
  13. package/dist/chunk-LEOORZQY.cjs.map +1 -0
  14. package/dist/chunk-ROA74LH6.cjs +61 -0
  15. package/dist/chunk-ROA74LH6.cjs.map +1 -0
  16. package/dist/chunk-WM5VVPED.js +61 -0
  17. package/dist/chunk-WM5VVPED.js.map +1 -0
  18. package/dist/chunk-X535MU7Z.js +5 -0
  19. package/dist/chunk-X535MU7Z.js.map +1 -0
  20. package/dist/chunk-ZADUJHVR.cjs +5 -0
  21. package/dist/chunk-ZADUJHVR.cjs.map +1 -0
  22. package/dist/chunk-ZECSTNEE.cjs +3 -0
  23. package/dist/chunk-ZECSTNEE.cjs.map +1 -0
  24. package/dist/chunk-ZVFEGG25.js +3 -0
  25. package/dist/chunk-ZVFEGG25.js.map +1 -0
  26. package/dist/components.cjs +1 -1
  27. package/dist/components.d.cts +2 -2
  28. package/dist/components.d.ts +2 -2
  29. package/dist/components.js +1 -1
  30. package/dist/index-Br7Sh9Ur.d.cts +329 -0
  31. package/dist/index-Br7Sh9Ur.d.ts +329 -0
  32. package/dist/{index-DAGfo2Fc.d.ts → index-CKNcgAj8.d.ts} +10 -2
  33. package/dist/{index-RBSUcdqN.d.cts → index-Wgogf4CX.d.cts} +10 -2
  34. package/dist/index.cjs +1 -1
  35. package/dist/index.d.cts +7 -4
  36. package/dist/index.d.ts +7 -4
  37. package/dist/index.js +1 -1
  38. package/dist/og.cjs +2 -0
  39. package/dist/og.cjs.map +1 -0
  40. package/dist/og.d.cts +5 -0
  41. package/dist/og.d.ts +5 -0
  42. package/dist/og.js +2 -0
  43. package/dist/og.js.map +1 -0
  44. package/dist/schema.cjs +1 -1
  45. package/dist/schema.d.cts +6 -2
  46. package/dist/schema.d.ts +6 -2
  47. package/dist/schema.js +1 -1
  48. package/dist/sitemap.cjs +2 -0
  49. package/dist/sitemap.cjs.map +1 -0
  50. package/dist/sitemap.d.cts +12 -0
  51. package/dist/sitemap.d.ts +12 -0
  52. package/dist/sitemap.js +2 -0
  53. package/dist/sitemap.js.map +1 -0
  54. package/dist/validation.cjs +2 -0
  55. package/dist/validation.cjs.map +1 -0
  56. package/dist/validation.d.cts +8 -0
  57. package/dist/validation.d.ts +8 -0
  58. package/dist/validation.js +2 -0
  59. package/dist/validation.js.map +1 -0
  60. package/package.json +38 -2
  61. package/dist/chunk-63ETSZTD.cjs +0 -3
  62. package/dist/chunk-63ETSZTD.cjs.map +0 -1
  63. package/dist/chunk-AYIAPQTP.js +0 -2
  64. package/dist/chunk-AYIAPQTP.js.map +0 -1
  65. package/dist/chunk-ES4OXVOR.js +0 -3
  66. package/dist/chunk-ES4OXVOR.js.map +0 -1
  67. package/dist/chunk-T7EB7Y2S.cjs +0 -2
  68. package/dist/chunk-T7EB7Y2S.cjs.map +0 -1
  69. package/dist/index-Dr2yktvz.d.cts +0 -136
  70. 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>
@@ -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 />
@@ -588,13 +784,6 @@ const combined = composeSchemas(
588
784
 
589
785
  ### Next.js App Router
590
786
 
591
- Use the `toNextMetadata()` adapter to convert any `SEOConfig` directly into a Next.js `Metadata` object.
592
-
593
- ```bash
594
- # no extra install needed — adapter is included in the package
595
- import { toNextMetadata } from 'react-ssr-seo-toolkit/adapters/nextjs';
596
- ```
597
-
598
787
  ```tsx
599
788
  // app/blog/[slug]/page.tsx
600
789
  import { toNextMetadata } from "react-ssr-seo-toolkit/adapters/nextjs";
@@ -603,7 +792,6 @@ import type { Metadata } from "next";
603
792
 
604
793
  export async function generateMetadata({ params }): Promise<Metadata> {
605
794
  const post = await getPost(params.slug);
606
-
607
795
  return toNextMetadata({
608
796
  title: post.title,
609
797
  titleTemplate: "%s | My Blog",
@@ -614,63 +802,8 @@ export async function generateMetadata({ params }): Promise<Metadata> {
614
802
  type: "article",
615
803
  images: [{ url: post.image, width: 1200, height: 630, alt: post.title }],
616
804
  },
617
- twitter: { card: "summary_large_image", creator: "@myblog" },
618
- robots: { index: true, follow: true },
619
- });
620
- }
621
-
622
- export default async function BlogPost({ params }) {
623
- const post = await getPost(params.slug);
624
-
625
- const schema = createArticleSchema({
626
- headline: post.title,
627
- url: `https://myblog.com/blog/${params.slug}`,
628
- datePublished: post.date,
629
- author: { name: post.author },
805
+ twitter: { card: "summary_large_image" },
630
806
  });
631
-
632
- return (
633
- <>
634
- <script
635
- type="application/ld+json"
636
- dangerouslySetInnerHTML={{ __html: safeJsonLdSerialize(schema) }}
637
- />
638
- <article>
639
- <h1>{post.title}</h1>
640
- <p>{post.content}</p>
641
- </article>
642
- </>
643
- );
644
- }
645
- ```
646
-
647
- <br />
648
-
649
- ### Next.js Pages Router
650
-
651
- ```tsx
652
- // pages/about.tsx — no <html> tags, Next.js handles that
653
- import Head from "next/head";
654
- import { SEOHead, mergeSEOConfig } from "react-ssr-seo-toolkit";
655
- import { siteConfig } from "../config/seo";
656
-
657
- export default function AboutPage() {
658
- const seo = mergeSEOConfig(siteConfig, {
659
- title: "About Us",
660
- description: "Learn about our mission.",
661
- canonical: "https://mysite.com/about",
662
- });
663
-
664
- return (
665
- <>
666
- <Head>
667
- <SEOHead {...seo} />
668
- </Head>
669
- <main>
670
- <h1>About Us</h1>
671
- </main>
672
- </>
673
- );
674
807
  }
675
808
  ```
676
809
 
@@ -678,14 +811,6 @@ export default function AboutPage() {
678
811
 
679
812
  ### React Router 7
680
813
 
681
- Use the `toRouterMeta()` adapter to convert any `SEOConfig` into React Router 7's `MetaDescriptor[]` array — the same format returned by a route's `meta()` export.
682
-
683
- ```bash
684
- import { toRouterMeta, toRouterLinks } from 'react-ssr-seo-toolkit/adapters/react-router';
685
- ```
686
-
687
- **Option A — `meta()` export (recommended, handles everything in one place):**
688
-
689
814
  ```tsx
690
815
  // app/routes/about.tsx
691
816
  import { toRouterMeta } from "react-ssr-seo-toolkit/adapters/react-router";
@@ -705,79 +830,6 @@ export function meta({}: Route.MetaArgs) {
705
830
  );
706
831
  }
707
832
 
708
- export default function AboutPage() {
709
- return (
710
- <main>
711
- <h1>About Us</h1>
712
- </main>
713
- );
714
- }
715
- ```
716
-
717
- **Option B — `meta()` + `links()` separated:**
718
-
719
- ```tsx
720
- // app/routes/about.tsx
721
- import { toRouterMeta, toRouterLinks } from "react-ssr-seo-toolkit/adapters/react-router";
722
-
723
- const seoConfig = {
724
- title: "About Us",
725
- canonical: "https://mysite.com/about",
726
- alternates: [{ hreflang: "en", href: "https://mysite.com/en/about" }],
727
- openGraph: { type: "website", title: "About Us" },
728
- };
729
-
730
- // meta tags (title, og:*, twitter:*, robots)
731
- export function meta() {
732
- return toRouterMeta(seoConfig);
733
- }
734
-
735
- // link tags (canonical, hreflang alternates)
736
- export function links() {
737
- return toRouterLinks(seoConfig);
738
- }
739
- ```
740
-
741
- **Option C — Classic loader pattern with `<SEOHead>`:**
742
-
743
- ```tsx
744
- // app/root.tsx — only the root layout writes <html>
745
- import { Outlet, useMatches } from "react-router";
746
- import { SEOHead } from "react-ssr-seo-toolkit";
747
-
748
- export default function Root() {
749
- const matches = useMatches();
750
- const seo = matches.at(-1)?.data?.seo;
751
-
752
- return (
753
- <html lang="en">
754
- <head>
755
- <meta charSet="utf-8" />
756
- <meta name="viewport" content="width=device-width, initial-scale=1" />
757
- {seo && <SEOHead {...seo} />}
758
- </head>
759
- <body>
760
- <Outlet />
761
- </body>
762
- </html>
763
- );
764
- }
765
- ```
766
-
767
- ```tsx
768
- // app/routes/about.tsx
769
- import { mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
770
- import { siteConfig, SITE_URL } from "../config/seo";
771
-
772
- export function loader() {
773
- return {
774
- seo: mergeSEOConfig(siteConfig, {
775
- title: "About",
776
- canonical: buildCanonicalUrl(SITE_URL, "/about"),
777
- }),
778
- };
779
- }
780
-
781
833
  export default function AboutPage() {
782
834
  return <main><h1>About Us</h1></main>;
783
835
  }
@@ -788,54 +840,26 @@ export default function AboutPage() {
788
840
  ### Express + React SSR
789
841
 
790
842
  ```tsx
791
- // server.tsx renders page components that include Document internally
792
- import express from "express";
793
- import { renderToString } from "react-dom/server";
794
- import { HomePage } from "./pages/HomePage";
795
- import { ProductPage } from "./pages/ProductPage";
796
-
797
- const app = express();
798
-
799
- app.get("/", (req, res) => {
800
- const html = renderToString(<HomePage />);
801
- 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
+ );
802
848
  });
803
849
 
804
- app.get("/products/:id", (req, res) => {
805
- const product = getProduct(req.params.id);
806
- const html = renderToString(<ProductPage product={product} />);
807
- 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
+ );
808
855
  });
809
856
 
810
- app.listen(3000);
811
- ```
812
-
813
- ```tsx
814
- // pages/ProductPage.tsx — no <html> tags, Document handles that
815
- import { mergeSEOConfig, createProductSchema } from "react-ssr-seo-toolkit";
816
- import { siteConfig } from "../config/seo";
817
- import { Document } from "../components/Document";
818
-
819
- export function ProductPage({ product }) {
820
- const seo = mergeSEOConfig(siteConfig, {
821
- title: product.name,
822
- description: product.description,
823
- canonical: product.url,
824
- });
825
-
826
- const schema = createProductSchema({
827
- name: product.name,
828
- url: product.url,
829
- price: product.price,
830
- });
831
-
832
- return (
833
- <Document seo={seo} schemas={[schema]}>
834
- <h1>{product.name}</h1>
835
- <p>${product.price}</p>
836
- </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" })
837
861
  );
838
- }
862
+ });
839
863
  ```
840
864
 
841
865
  <br />
@@ -851,7 +875,7 @@ export function ProductPage({ product }) {
851
875
  | Function | What It Does |
852
876
  |---|---|
853
877
  | `createSEOConfig(config?)` | Create a normalized SEO config. Use for site-wide defaults. |
854
- | `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. |
855
879
  | `normalizeSEOConfig(config)` | Trim strings, normalize URLs, clean up a config object. |
856
880
 
857
881
  <br />
@@ -861,70 +885,101 @@ export function ProductPage({ product }) {
861
885
  | Function | Example | Result |
862
886
  |---|---|---|
863
887
  | `buildTitle(title, template)` | `buildTitle("About", "%s \| MySite")` | `"About \| MySite"` |
864
- | `buildDescription(desc, maxLen)` | `buildDescription("Long text...", 160)` | Truncated at 160 chars |
865
888
  | `buildCanonicalUrl(base, path)` | `buildCanonicalUrl("https://x.com", "/about")` | `"https://x.com/about"` |
866
889
  | `buildRobotsDirectives(config)` | `buildRobotsDirectives({ index: false })` | `"noindex, follow"` |
867
- | `noIndex()` | `noIndex()` | `{ index: false, follow: true }` |
868
- | `noIndexNoFollow()` | `noIndexNoFollow()` | `{ index: false, follow: false }` |
869
- | `buildOpenGraph(config)` | `buildOpenGraph({ title: "Hi" })` | `[{ property: "og:title", content: "Hi" }]` |
870
- | `buildTwitterMetadata(config)` | `buildTwitterMetadata({ card: "summary" })` | `[{ name: "twitter:card", content: "summary" }]` |
871
- | `buildAlternateLinks(alternates)` | `buildAlternateLinks([{ hreflang: "en", href: "..." }])` | `[{ rel: "alternate", hreflang: "en", href: "..." }]` |
890
+ | `noIndex()` | | `{ index: false, follow: true }` |
891
+ | `noIndexNoFollow()` | | `{ index: false, follow: false }` |
872
892
 
873
893
  <br />
874
894
 
875
895
  ### JSON-LD Schema Generators
876
896
 
877
- All return a plain object with `@context: "https://schema.org"` and `@type` set.
897
+ All return `{ "@context": "https://schema.org", "@type": "...", ... }`.
878
898
 
879
899
  | Function | Schema Type | Use Case |
880
900
  |---|---|---|
881
901
  | `createOrganizationSchema(input)` | Organization | Company info, logo, social links, contact |
882
902
  | `createWebsiteSchema(input)` | WebSite | Site name, sitelinks searchbox |
883
- | `createArticleSchema(input)` | Article | Blog posts, news articles, authors, dates |
884
- | `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 |
885
905
  | `createBreadcrumbSchema(items)` | BreadcrumbList | Navigation hierarchy |
886
- | `createFAQSchema(items)` | FAQPage | FAQ pages with question + answer pairs |
887
- | `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>` |
888
912
 
889
913
  <br />
890
914
 
891
- ### Utilities
915
+ ### Sitemap & Robots — `react-ssr-seo-toolkit/sitemap`
892
916
 
893
917
  | Function | What It Does |
894
918
  |---|---|
895
- | `safeJsonLdSerialize(data)` | Serialize JSON-LD safely escapes `<`, `>`, `&` to prevent XSS |
896
- | `normalizeUrl(url)` | Trim whitespace, remove trailing slashes |
897
- | `buildFullUrl(base, path?)` | Combine base URL with path |
898
- | `omitEmpty(obj)` | Remove keys with `undefined`, `null`, or empty string values |
899
- | `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` |
900
922
 
901
- <br />
923
+ **`generateSitemap` options:**
902
924
 
903
- ### Framework Adapters
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 />
904
934
 
905
- #### Next.js Adapter — `react-ssr-seo-toolkit/adapters/nextjs`
935
+ ### SEO Validation & Scoring — `react-ssr-seo-toolkit/validation`
906
936
 
907
937
  | Function | What It Does |
908
938
  |---|---|
909
- | `toNextMetadata(config)` | Converts `SEOConfig` Next.js App Router `Metadata` object. Use inside `generateMetadata()` or `export const metadata`. |
910
- | `buildNextTitle(config)` | Returns the resolved title string (applies `titleTemplate` if set). |
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):**
911
945
 
912
- **Exported types:** `NextJSMetadata`, `NextJSMetadataTitle`, `NextJSMetadataImage`, `NextJSMetadataRobots`, `NextJSMetadataOpenGraph`, `NextJSMetadataTwitter`, `NextJSMetadataAlternates`
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 |
913
958
 
914
959
  <br />
915
960
 
916
- #### React Router 7 Adapter — `react-ssr-seo-toolkit/adapters/react-router`
961
+ ### OG Image Generation — `react-ssr-seo-toolkit/og`
917
962
 
918
963
  | Function | What It Does |
919
964
  |---|---|
920
- | `toRouterMeta(config)` | Converts `SEOConfig` `MetaDescriptor[]`. Use as the return value of a route's `meta()` export. Includes title, description, robots, OG, Twitter, hreflang, canonical, and custom tags. |
921
- | `toRouterLinks(config)` | Converts canonical + hreflang + additionalLinkTags → `LinkDescriptor[]`. Use as the return value of a route's `links()` export. |
965
+ | `createOGImageSVG(options)` | Returns a 1200×630 SVG string serve as `image/svg+xml` or convert to PNG with `sharp` |
922
966
 
923
- **Exported types:** `RouterMetaDescriptor`, `RouterTitleDescriptor`, `RouterNameMetaDescriptor`, `RouterPropertyMetaDescriptor`, `RouterLinkDescriptor`
967
+ **Options:**
924
968
 
925
- <br />
926
-
927
- ---
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) |
928
983
 
929
984
  <br />
930
985
 
@@ -932,42 +987,17 @@ All return a plain object with `@context: "https://schema.org"` and `@type` set.
932
987
 
933
988
  #### `<SEOHead>`
934
989
 
935
- 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.
936
991
 
937
992
  ```tsx
938
993
  <SEOHead
939
994
  title="My Page"
940
995
  titleTemplate="%s | MySite"
941
- description="Page description here."
996
+ description="Page description."
942
997
  canonical="https://mysite.com/page"
943
998
  robots={{ index: true, follow: true }}
944
- openGraph={{
945
- title: "My Page",
946
- description: "For social sharing.",
947
- type: "website",
948
- url: "https://mysite.com/page",
949
- siteName: "MySite",
950
- locale: "en_US",
951
- images: [{ url: "https://mysite.com/og.jpg", width: 1200, height: 630, alt: "Preview" }],
952
- }}
953
- twitter={{
954
- card: "summary_large_image",
955
- site: "@mysite",
956
- creator: "@author",
957
- title: "My Page",
958
- image: "https://mysite.com/twitter.jpg",
959
- }}
960
- alternates={[
961
- { hreflang: "en", href: "https://mysite.com/en/page" },
962
- { hreflang: "es", href: "https://mysite.com/es/page" },
963
- ]}
964
- additionalMetaTags={[
965
- { name: "author", content: "Jane Doe" },
966
- ]}
967
- additionalLinkTags={[
968
- { rel: "icon", href: "/favicon.ico" },
969
- ]}
970
- jsonLd={createArticleSchema({ headline: "...", url: "..." })}
999
+ openGraph={{ type: "website", images: [{ url: "...", width: 1200, height: 630 }] }}
1000
+ twitter={{ card: "summary_large_image", site: "@mysite" }}
971
1001
  />
972
1002
  ```
973
1003
 
@@ -979,6 +1009,53 @@ Standalone JSON-LD `<script>` tag renderer.
979
1009
  <JsonLd data={createProductSchema({ name: "Widget", url: "...", price: 29.99 })} />
980
1010
  ```
981
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
+
982
1059
  <br />
983
1060
 
984
1061
  ### TypeScript Types
@@ -986,22 +1063,28 @@ Standalone JSON-LD `<script>` tag renderer.
986
1063
  ```tsx
987
1064
  import type {
988
1065
  SEOConfig,
989
- OpenGraphConfig,
990
- OpenGraphImage,
991
- OpenGraphType, // "website" | "article" | "product" | "profile" | ...
992
- TwitterConfig,
993
- TwitterCardType, // "summary" | "summary_large_image" | "app" | "player"
1066
+ OpenGraphConfig, OpenGraphImage, OpenGraphType,
1067
+ TwitterConfig, TwitterCardType,
994
1068
  RobotsConfig,
995
1069
  AlternateLink,
996
- JSONLDBase,
997
- BreadcrumbItem,
998
- OrganizationSchemaInput,
999
- WebsiteSchemaInput,
1000
- ArticleSchemaInput,
1001
- ProductSchemaInput,
1070
+ JSONLDBase, BreadcrumbItem,
1071
+ OrganizationSchemaInput, WebsiteSchemaInput,
1072
+ ArticleSchemaInput, ProductSchemaInput,
1002
1073
  FAQItem,
1003
- SEOHeadProps,
1004
- 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,
1005
1088
  } from "react-ssr-seo-toolkit";
1006
1089
  ```
1007
1090
 
@@ -1011,9 +1094,28 @@ import type {
1011
1094
 
1012
1095
  <br />
1013
1096
 
1014
- ## Live Demo
1097
+ ## Entry Points
1098
+
1099
+ The package ships separate entry points so you only import what you need:
1015
1100
 
1016
- The repo includes a **working Express SSR demo** with every feature:
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
+ ---
1115
+
1116
+ <br />
1117
+
1118
+ ## Live Demo
1017
1119
 
1018
1120
  ```bash
1019
1121
  git clone https://github.com/Tonmoy01/react-ssr-seo-toolkit.git
@@ -1022,19 +1124,7 @@ npm install
1022
1124
  npm run demo
1023
1125
  ```
1024
1126
 
1025
- Then visit [http://localhost:3000](http://localhost:3000):
1026
-
1027
- | URL | Page | SEO Features |
1028
- |---|---|---|
1029
- | `/` | Home | Organization + Website schema, hreflang, OG images |
1030
- | `/getting-started` | Getting Started | Installation guide with copy-paste examples |
1031
- | `/article` | Article | Article schema, breadcrumbs, multiple authors, Twitter cards |
1032
- | `/product` | Product | Product schema, pricing, ratings, availability |
1033
- | `/faq` | FAQ | FAQPage schema with Q&A pairs |
1034
- | `/noindex` | No-Index | Robots noindex directive |
1035
- | `/api` | API Reference | Complete function and type documentation |
1036
-
1037
- > **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).
1038
1128
 
1039
1129
  <br />
1040
1130
 
@@ -1049,9 +1139,7 @@ npm install # install dependencies
1049
1139
  npm run build # build the library
1050
1140
  npm run dev # watch mode (auto-rebuild)
1051
1141
  npm test # run tests
1052
- npm run test:watch # tests in watch mode
1053
1142
  npm run lint # type check
1054
- npm run clean # clean build output
1055
1143
  npm run demo # run demo server
1056
1144
  ```
1057
1145
 
@@ -1065,24 +1153,16 @@ npm run demo # run demo server
1065
1153
 
1066
1154
  ### "Cannot find module 'react-ssr-seo-toolkit'"
1067
1155
 
1068
- 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`.
1069
1157
 
1070
1158
  ### Hydration mismatch warnings
1071
1159
 
1072
- `<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.
1073
1161
 
1074
1162
  ### JSON-LD not appearing in page source
1075
1163
 
1076
1164
  Make sure `<JsonLd>` is inside `<head>` and rendered during SSR — not in a client-only `useEffect`.
1077
1165
 
1078
- ### TypeScript errors
1079
-
1080
- All types are exported. Import them directly:
1081
-
1082
- ```tsx
1083
- import type { SEOConfig, OpenGraphConfig } from "react-ssr-seo-toolkit";
1084
- ```
1085
-
1086
1166
  <br />
1087
1167
 
1088
1168
  ---