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.
- package/README.md +633 -460
- package/dist/adapters/nextjs.cjs +2 -0
- package/dist/adapters/nextjs.cjs.map +1 -0
- package/dist/adapters/nextjs.d.cts +86 -0
- package/dist/adapters/nextjs.d.ts +86 -0
- package/dist/adapters/nextjs.js +2 -0
- package/dist/adapters/nextjs.js.map +1 -0
- package/dist/adapters/react-router.cjs +2 -0
- package/dist/adapters/react-router.cjs.map +1 -0
- package/dist/adapters/react-router.d.cts +71 -0
- package/dist/adapters/react-router.d.ts +71 -0
- package/dist/adapters/react-router.js +2 -0
- package/dist/adapters/react-router.js.map +1 -0
- package/dist/chunk-3UCLUFOI.js +5 -0
- package/dist/chunk-3UCLUFOI.js.map +1 -0
- package/dist/chunk-A62THBIS.cjs +3 -0
- package/dist/chunk-A62THBIS.cjs.map +1 -0
- package/dist/chunk-FKDMHECL.cjs +2 -0
- package/dist/chunk-FKDMHECL.cjs.map +1 -0
- package/dist/chunk-GNLXGAS6.js +3 -0
- package/dist/chunk-GNLXGAS6.js.map +1 -0
- package/dist/chunk-LEOORZQY.cjs +5 -0
- package/dist/chunk-LEOORZQY.cjs.map +1 -0
- package/dist/chunk-LP66SUGR.js +2 -0
- package/dist/chunk-LP66SUGR.js.map +1 -0
- package/dist/chunk-ROA74LH6.cjs +61 -0
- package/dist/chunk-ROA74LH6.cjs.map +1 -0
- package/dist/chunk-WM5VVPED.js +61 -0
- package/dist/chunk-WM5VVPED.js.map +1 -0
- package/dist/chunk-X535MU7Z.js +5 -0
- package/dist/chunk-X535MU7Z.js.map +1 -0
- package/dist/chunk-ZADUJHVR.cjs +5 -0
- package/dist/chunk-ZADUJHVR.cjs.map +1 -0
- package/dist/chunk-ZECSTNEE.cjs +3 -0
- package/dist/chunk-ZECSTNEE.cjs.map +1 -0
- package/dist/chunk-ZVFEGG25.js +3 -0
- package/dist/chunk-ZVFEGG25.js.map +1 -0
- package/dist/components.cjs +1 -1
- package/dist/components.d.cts +2 -2
- package/dist/components.d.ts +2 -2
- package/dist/components.js +1 -1
- package/dist/index-Br7Sh9Ur.d.cts +329 -0
- package/dist/index-Br7Sh9Ur.d.ts +329 -0
- package/dist/{index-DAGfo2Fc.d.ts → index-CKNcgAj8.d.ts} +10 -2
- package/dist/{index-RBSUcdqN.d.cts → index-Wgogf4CX.d.cts} +10 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1 -1
- package/dist/og.cjs +2 -0
- package/dist/og.cjs.map +1 -0
- package/dist/og.d.cts +5 -0
- package/dist/og.d.ts +5 -0
- package/dist/og.js +2 -0
- package/dist/og.js.map +1 -0
- package/dist/schema.cjs +1 -1
- package/dist/schema.d.cts +6 -2
- package/dist/schema.d.ts +6 -2
- package/dist/schema.js +1 -1
- package/dist/sitemap.cjs +2 -0
- package/dist/sitemap.cjs.map +1 -0
- package/dist/sitemap.d.cts +12 -0
- package/dist/sitemap.d.ts +12 -0
- package/dist/sitemap.js +2 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/validation.cjs +2 -0
- package/dist/validation.cjs.map +1 -0
- package/dist/validation.d.cts +8 -0
- package/dist/validation.d.ts +8 -0
- package/dist/validation.js +2 -0
- package/dist/validation.js.map +1 -0
- package/package.json +58 -2
- package/dist/chunk-63ETSZTD.cjs +0 -3
- package/dist/chunk-63ETSZTD.cjs.map +0 -1
- package/dist/chunk-ES4OXVOR.js +0 -3
- package/dist/chunk-ES4OXVOR.js.map +0 -1
- package/dist/chunk-QBHCTDUJ.cjs +0 -2
- package/dist/chunk-QBHCTDUJ.cjs.map +0 -1
- package/dist/chunk-YMCW2G4X.js +0 -2
- package/dist/chunk-YMCW2G4X.js.map +0 -1
- package/dist/index-Dr2yktvz.d.cts +0 -136
- 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
|
+
<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** • **Open Graph** • **Twitter Cards** • **JSON-LD** • **Canonical URLs** • **Hreflang** • **Robots**
|
|
26
|
+
**Meta Tags** • **Open Graph** • **Twitter Cards** • **JSON-LD** • **Canonical URLs** • **Hreflang** • **Robots** • **Sitemap** • **SEO Validation** • **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
|
-
-
|
|
54
|
-
-
|
|
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** —
|
|
64
|
-
- **
|
|
65
|
-
- **
|
|
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** | `
|
|
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
|
|
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",
|
|
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.**
|
|
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
|
-
|
|
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:
|
|
359
|
-
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:
|
|
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:
|
|
599
|
+
name: "Ergonomic Mechanical Keyboard",
|
|
372
600
|
url,
|
|
373
|
-
|
|
374
|
-
price: product.price,
|
|
601
|
+
price: 189.99,
|
|
375
602
|
priceCurrency: "USD",
|
|
376
|
-
availability:
|
|
377
|
-
brand:
|
|
378
|
-
sku:
|
|
379
|
-
images: [
|
|
380
|
-
ratingValue:
|
|
381
|
-
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>
|
|
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
|
-
###
|
|
625
|
+
### Job Posting Schema
|
|
400
626
|
|
|
401
627
|
```tsx
|
|
402
|
-
import {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
416
|
-
title: "FAQ",
|
|
417
|
-
description: "Frequently asked questions about our products and services.",
|
|
418
|
-
canonical: buildCanonicalUrl(SITE_URL, "/faq"),
|
|
419
|
-
});
|
|
653
|
+
---
|
|
420
654
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
###
|
|
689
|
+
### Person Schema
|
|
442
690
|
|
|
443
691
|
```tsx
|
|
444
|
-
import {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
|
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
|
-
|
|
595
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
description:
|
|
604
|
-
|
|
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
|
-
###
|
|
812
|
+
### React Router 7
|
|
643
813
|
|
|
644
814
|
```tsx
|
|
645
|
-
//
|
|
646
|
-
import
|
|
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
|
|
704
|
-
return
|
|
705
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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("/
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
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.
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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.
|
|
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()` |
|
|
803
|
-
| `noIndexNoFollow()` |
|
|
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
|
|
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
|
|
819
|
-
| `createProductSchema(input)` | Product | E-commerce: price, brand, SKU, ratings
|
|
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 |
|
|
822
|
-
| `
|
|
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
|
-
###
|
|
915
|
+
### Sitemap & Robots — `react-ssr-seo-toolkit/sitemap`
|
|
827
916
|
|
|
828
917
|
| Function | What It Does |
|
|
829
918
|
|---|---|
|
|
830
|
-
| `
|
|
831
|
-
| `
|
|
832
|
-
| `
|
|
833
|
-
|
|
834
|
-
|
|
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
|
|
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
|
|
996
|
+
description="Page description."
|
|
849
997
|
canonical="https://mysite.com/page"
|
|
850
998
|
robots={{ index: true, follow: true }}
|
|
851
|
-
openGraph={{
|
|
852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
WebsiteSchemaInput,
|
|
907
|
-
ArticleSchemaInput,
|
|
908
|
-
ProductSchemaInput,
|
|
1070
|
+
JSONLDBase, BreadcrumbItem,
|
|
1071
|
+
OrganizationSchemaInput, WebsiteSchemaInput,
|
|
1072
|
+
ArticleSchemaInput, ProductSchemaInput,
|
|
909
1073
|
FAQItem,
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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`.
|
|
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.
|
|
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
|
---
|