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