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.
Files changed (2) hide show
  1. package/README.md +147 -107
  2. 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.3-000000?style=for-the-badge&labelColor=000000" alt="react-ssr-seo-toolkit" />
5
+ <img src="https://img.shields.io/badge/react--ssr--seo--toolkit-v1.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
- You only need **two new files** — a shared SEO config and your page components:
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 ← shared SEO defaults (Step 3)
112
+ │ └── seo.ts ← site-wide SEO defaults
113
+ ├── components/
114
+ │ └── Document.tsx ← handles <html>, <head>, <SEOHead>, <body>
113
115
  ├── pages/
114
- │ ├── HomePage.tsx ← each page merges its own SEO
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, then render with `<SEOHead>`.
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 { SEOHead, mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
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
- <html>
165
- <head>
166
- <SEOHead {...seo} />
167
- </head>
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
- <html>
251
- <head>
252
- <SEOHead {...seo} />
253
- <JsonLd data={article} />
254
- <JsonLd data={breadcrumbs} />
255
- </head>
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, createBreadcrumbSchema,
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
- <html>
357
- <head>
358
- <SEOHead {...seo} />
359
- <JsonLd data={schema} />
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
- <html>
400
- <head>
401
- <SEOHead {...seo} />
402
- <JsonLd data={createFAQSchema(faqs)} />
403
- </head>
404
- <body>
405
- <h1>Frequently Asked Questions</h1>
406
- {faqs.map((faq, i) => (
407
- <details key={i}>
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
- <html>
473
- <head>
474
- <SEOHead {...seo} />
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 { SEOHead, mergeSEOConfig, createOrganizationSchema, JsonLd } from "react-ssr-seo-toolkit";
666
- import { siteConfig } from "./config/seo";
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 HomePage() {
669
- const seo = mergeSEOConfig(siteConfig, {
670
- title: "Home",
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
- <h1>Welcome to Acme</h1>
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 { SEOHead, JsonLd, mergeSEOConfig, createProductSchema } from "react-ssr-seo-toolkit";
698
- import { siteConfig } from "./config/seo";
729
+ import { HomePage } from "./pages/HomePage";
730
+ import { ProductPage } from "./pages/ProductPage";
699
731
 
700
732
  const app = express();
701
733
 
702
- function ProductPage({ product }) {
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
- <html>
711
- <head>
712
- <SEOHead {...seo} />
713
- <JsonLd data={createProductSchema({
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",
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
  },