react-ssr-seo-toolkit 1.0.3 → 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 +147 -107
- package/package.json +2 -2
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.0.4-000000?style=for-the-badge&labelColor=000000" alt="react-ssr-seo-toolkit" />
|
|
6
6
|
|
|
7
7
|
<br />
|
|
8
8
|
<br />
|
|
@@ -104,18 +104,18 @@ npm install react-ssr-seo-toolkit
|
|
|
104
104
|
|
|
105
105
|
### 2. Project Structure
|
|
106
106
|
|
|
107
|
-
|
|
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
108
|
|
|
109
109
|
```
|
|
110
110
|
my-app/
|
|
111
111
|
├── config/
|
|
112
|
-
│ └── seo.ts ←
|
|
112
|
+
│ └── seo.ts ← site-wide SEO defaults
|
|
113
|
+
├── components/
|
|
114
|
+
│ └── Document.tsx ← handles <html>, <head>, <SEOHead>, <body>
|
|
113
115
|
├── pages/
|
|
114
|
-
│ ├── HomePage.tsx ←
|
|
116
|
+
│ ├── HomePage.tsx ← just content + SEO config (no <html> tags!)
|
|
115
117
|
│ ├── AboutPage.tsx
|
|
116
118
|
│ └── BlogPost.tsx
|
|
117
|
-
├── components/
|
|
118
|
-
│ └── Layout.tsx ← wraps <SEOHead> + <JsonLd>
|
|
119
119
|
├── server.tsx ← Express / SSR entry point
|
|
120
120
|
└── package.json
|
|
121
121
|
```
|
|
@@ -144,14 +144,54 @@ export const SITE_URL = "https://mysite.com";
|
|
|
144
144
|
|
|
145
145
|
<br />
|
|
146
146
|
|
|
147
|
+
### 3.5. Create a Document Component
|
|
148
|
+
|
|
149
|
+
The Document handles `<html>`, `<head>`, `<SEOHead>`, and `<body>` — so pages never have to.
|
|
150
|
+
|
|
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
|
+
}
|
|
161
|
+
|
|
162
|
+
export function Document({ children, seo, schemas }: DocumentProps) {
|
|
163
|
+
return (
|
|
164
|
+
<html lang="en">
|
|
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" />
|
|
169
|
+
<SEOHead {...seo} />
|
|
170
|
+
{schemas?.map((schema, i) => <JsonLd key={i} data={schema} />)}
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
<nav>{/* shared navigation */}</nav>
|
|
174
|
+
<main>{children}</main>
|
|
175
|
+
<footer>{/* shared footer */}</footer>
|
|
176
|
+
</body>
|
|
177
|
+
</html>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
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
|
+
|
|
147
186
|
### 4. Add to Any Page
|
|
148
187
|
|
|
149
|
-
Merge the shared config with page-specific values
|
|
188
|
+
Merge the shared config with page-specific values. **No `<html>` or `<head>` tags needed** — the Document handles that.
|
|
150
189
|
|
|
151
190
|
```tsx
|
|
152
191
|
// pages/AboutPage.tsx
|
|
153
|
-
import {
|
|
192
|
+
import { mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
|
|
154
193
|
import { siteConfig, SITE_URL } from "../config/seo";
|
|
194
|
+
import { Document } from "../components/Document";
|
|
155
195
|
|
|
156
196
|
export function AboutPage() {
|
|
157
197
|
const seo = mergeSEOConfig(siteConfig, {
|
|
@@ -161,14 +201,10 @@ export function AboutPage() {
|
|
|
161
201
|
});
|
|
162
202
|
|
|
163
203
|
return (
|
|
164
|
-
<
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<body>
|
|
169
|
-
<h1>About Us</h1>
|
|
170
|
-
</body>
|
|
171
|
-
</html>
|
|
204
|
+
<Document seo={seo}>
|
|
205
|
+
<h1>About Us</h1>
|
|
206
|
+
<p>Our story...</p>
|
|
207
|
+
</Document>
|
|
172
208
|
);
|
|
173
209
|
}
|
|
174
210
|
```
|
|
@@ -192,11 +228,11 @@ export function AboutPage() {
|
|
|
192
228
|
```tsx
|
|
193
229
|
// pages/BlogPost.tsx
|
|
194
230
|
import {
|
|
195
|
-
SEOHead, JsonLd,
|
|
196
231
|
mergeSEOConfig, buildCanonicalUrl,
|
|
197
232
|
createArticleSchema, createBreadcrumbSchema,
|
|
198
233
|
} from "react-ssr-seo-toolkit";
|
|
199
234
|
import { siteConfig, SITE_URL } from "../config/seo";
|
|
235
|
+
import { Document } from "../components/Document";
|
|
200
236
|
|
|
201
237
|
export function BlogPostPage() {
|
|
202
238
|
// ── Page SEO ──────────────────────────────────────────────
|
|
@@ -245,21 +281,14 @@ export function BlogPostPage() {
|
|
|
245
281
|
{ name: "How to Build an SSR App", url: "https://myblog.com/blog/ssr-guide" },
|
|
246
282
|
]);
|
|
247
283
|
|
|
248
|
-
// ── Render
|
|
284
|
+
// ── Render — no <html> or <head> tags! ────────────────────
|
|
249
285
|
return (
|
|
250
|
-
<
|
|
251
|
-
<
|
|
252
|
-
<
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<body>
|
|
257
|
-
<article>
|
|
258
|
-
<h1>How to Build an SSR App</h1>
|
|
259
|
-
<p>Your article content here...</p>
|
|
260
|
-
</article>
|
|
261
|
-
</body>
|
|
262
|
-
</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>
|
|
263
292
|
);
|
|
264
293
|
}
|
|
265
294
|
```
|
|
@@ -304,11 +333,11 @@ export function BlogPostPage() {
|
|
|
304
333
|
|
|
305
334
|
```tsx
|
|
306
335
|
import {
|
|
307
|
-
SEOHead, JsonLd,
|
|
308
336
|
mergeSEOConfig, buildCanonicalUrl,
|
|
309
|
-
createProductSchema,
|
|
337
|
+
createProductSchema,
|
|
310
338
|
} from "react-ssr-seo-toolkit";
|
|
311
339
|
import { siteConfig, SITE_URL } from "../config/seo";
|
|
340
|
+
import { Document } from "../components/Document";
|
|
312
341
|
|
|
313
342
|
function ProductPage() {
|
|
314
343
|
const product = {
|
|
@@ -353,16 +382,10 @@ function ProductPage() {
|
|
|
353
382
|
});
|
|
354
383
|
|
|
355
384
|
return (
|
|
356
|
-
<
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
</head>
|
|
361
|
-
<body>
|
|
362
|
-
<h1>{product.name}</h1>
|
|
363
|
-
<p>${product.price}</p>
|
|
364
|
-
</body>
|
|
365
|
-
</html>
|
|
385
|
+
<Document seo={seo} schemas={[schema]}>
|
|
386
|
+
<h1>{product.name}</h1>
|
|
387
|
+
<p>${product.price}</p>
|
|
388
|
+
</Document>
|
|
366
389
|
);
|
|
367
390
|
}
|
|
368
391
|
```
|
|
@@ -377,10 +400,10 @@ function ProductPage() {
|
|
|
377
400
|
|
|
378
401
|
```tsx
|
|
379
402
|
import {
|
|
380
|
-
SEOHead, JsonLd,
|
|
381
403
|
mergeSEOConfig, buildCanonicalUrl, createFAQSchema,
|
|
382
404
|
} from "react-ssr-seo-toolkit";
|
|
383
405
|
import { siteConfig, SITE_URL } from "../config/seo";
|
|
406
|
+
import { Document } from "../components/Document";
|
|
384
407
|
|
|
385
408
|
function FAQPage() {
|
|
386
409
|
const faqs = [
|
|
@@ -396,21 +419,15 @@ function FAQPage() {
|
|
|
396
419
|
});
|
|
397
420
|
|
|
398
421
|
return (
|
|
399
|
-
<
|
|
400
|
-
<
|
|
401
|
-
|
|
402
|
-
<
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
<summary>{faq.question}</summary>
|
|
409
|
-
<p>{faq.answer}</p>
|
|
410
|
-
</details>
|
|
411
|
-
))}
|
|
412
|
-
</body>
|
|
413
|
-
</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>
|
|
414
431
|
);
|
|
415
432
|
}
|
|
416
433
|
```
|
|
@@ -425,11 +442,11 @@ function FAQPage() {
|
|
|
425
442
|
|
|
426
443
|
```tsx
|
|
427
444
|
import {
|
|
428
|
-
SEOHead, JsonLd,
|
|
429
445
|
mergeSEOConfig,
|
|
430
446
|
createOrganizationSchema, createWebsiteSchema,
|
|
431
447
|
} from "react-ssr-seo-toolkit";
|
|
432
448
|
import { siteConfig } from "../config/seo";
|
|
449
|
+
import { Document } from "../components/Document";
|
|
433
450
|
|
|
434
451
|
function HomePage() {
|
|
435
452
|
const seo = mergeSEOConfig(siteConfig, {
|
|
@@ -469,16 +486,9 @@ function HomePage() {
|
|
|
469
486
|
});
|
|
470
487
|
|
|
471
488
|
return (
|
|
472
|
-
<
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
<JsonLd data={org} />
|
|
476
|
-
<JsonLd data={site} />
|
|
477
|
-
</head>
|
|
478
|
-
<body>
|
|
479
|
-
<h1>Welcome to Acme</h1>
|
|
480
|
-
</body>
|
|
481
|
-
</html>
|
|
489
|
+
<Document seo={seo} schemas={[org, site]}>
|
|
490
|
+
<h1>Welcome to Acme</h1>
|
|
491
|
+
</Document>
|
|
482
492
|
);
|
|
483
493
|
}
|
|
484
494
|
```
|
|
@@ -632,9 +642,10 @@ export default function BlogPost({ params }) {
|
|
|
632
642
|
### Next.js Pages Router
|
|
633
643
|
|
|
634
644
|
```tsx
|
|
635
|
-
// pages/about.tsx
|
|
645
|
+
// pages/about.tsx — no <html> tags, Next.js handles that
|
|
636
646
|
import Head from "next/head";
|
|
637
647
|
import { SEOHead, mergeSEOConfig } from "react-ssr-seo-toolkit";
|
|
648
|
+
import { siteConfig } from "../config/seo";
|
|
638
649
|
|
|
639
650
|
export default function AboutPage() {
|
|
640
651
|
const seo = mergeSEOConfig(siteConfig, {
|
|
@@ -661,76 +672,105 @@ export default function AboutPage() {
|
|
|
661
672
|
### React Router 7 SSR
|
|
662
673
|
|
|
663
674
|
```tsx
|
|
664
|
-
// app/root.tsx
|
|
665
|
-
import {
|
|
666
|
-
import {
|
|
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";
|
|
667
678
|
|
|
668
|
-
export function
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
canonical: "https://acme.com",
|
|
672
|
-
});
|
|
679
|
+
export default function Root() {
|
|
680
|
+
const matches = useMatches();
|
|
681
|
+
const seo = matches.at(-1)?.data?.seo;
|
|
673
682
|
|
|
674
683
|
return (
|
|
675
684
|
<html lang="en">
|
|
676
685
|
<head>
|
|
677
686
|
<meta charSet="utf-8" />
|
|
678
687
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
679
|
-
<SEOHead {...seo} />
|
|
688
|
+
{seo && <SEOHead {...seo} />}
|
|
680
689
|
</head>
|
|
681
690
|
<body>
|
|
682
|
-
<
|
|
691
|
+
<Outlet />
|
|
683
692
|
</body>
|
|
684
693
|
</html>
|
|
685
694
|
);
|
|
686
695
|
}
|
|
687
696
|
```
|
|
688
697
|
|
|
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
|
+
```
|
|
720
|
+
|
|
689
721
|
<br />
|
|
690
722
|
|
|
691
723
|
### Express + React SSR
|
|
692
724
|
|
|
693
725
|
```tsx
|
|
694
|
-
// server.tsx
|
|
726
|
+
// server.tsx — renders page components that include Document internally
|
|
695
727
|
import express from "express";
|
|
696
728
|
import { renderToString } from "react-dom/server";
|
|
697
|
-
import {
|
|
698
|
-
import {
|
|
729
|
+
import { HomePage } from "./pages/HomePage";
|
|
730
|
+
import { ProductPage } from "./pages/ProductPage";
|
|
699
731
|
|
|
700
732
|
const app = express();
|
|
701
733
|
|
|
702
|
-
|
|
734
|
+
app.get("/", (req, res) => {
|
|
735
|
+
const html = renderToString(<HomePage />);
|
|
736
|
+
res.send(`<!DOCTYPE html>${html}`);
|
|
737
|
+
});
|
|
738
|
+
|
|
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 }) {
|
|
703
755
|
const seo = mergeSEOConfig(siteConfig, {
|
|
704
756
|
title: product.name,
|
|
705
757
|
description: product.description,
|
|
706
758
|
canonical: product.url,
|
|
707
759
|
});
|
|
708
760
|
|
|
761
|
+
const schema = createProductSchema({
|
|
762
|
+
name: product.name,
|
|
763
|
+
url: product.url,
|
|
764
|
+
price: product.price,
|
|
765
|
+
});
|
|
766
|
+
|
|
709
767
|
return (
|
|
710
|
-
<
|
|
711
|
-
<
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
name: product.name,
|
|
715
|
-
url: product.url,
|
|
716
|
-
price: product.price,
|
|
717
|
-
})} />
|
|
718
|
-
</head>
|
|
719
|
-
<body>
|
|
720
|
-
<h1>{product.name}</h1>
|
|
721
|
-
<p>${product.price}</p>
|
|
722
|
-
</body>
|
|
723
|
-
</html>
|
|
768
|
+
<Document seo={seo} schemas={[schema]}>
|
|
769
|
+
<h1>{product.name}</h1>
|
|
770
|
+
<p>${product.price}</p>
|
|
771
|
+
</Document>
|
|
724
772
|
);
|
|
725
773
|
}
|
|
726
|
-
|
|
727
|
-
app.get("/products/:id", (req, res) => {
|
|
728
|
-
const product = getProduct(req.params.id);
|
|
729
|
-
const html = renderToString(<ProductPage product={product} />);
|
|
730
|
-
res.send(`<!DOCTYPE html>${html}`);
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
app.listen(3000);
|
|
734
774
|
```
|
|
735
775
|
|
|
736
776
|
<br />
|
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",
|
|
@@ -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
|
},
|