react-seo-optimize 1.0.0 → 2.0.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 CHANGED
@@ -1,14 +1,25 @@
1
1
  # react-seo-optimize
2
2
 
3
+ [![Tests](https://github.com/liljemery/react-seo-optimize/workflows/Tests/badge.svg)](https://github.com/liljemery/react-seo-optimize/actions)
4
+ [![npm version](https://img.shields.io/npm/v/react-seo-optimize.svg)](https://www.npmjs.com/package/react-seo-optimize)
5
+ [![npm downloads](https://img.shields.io/npm/dm/react-seo-optimize.svg)](https://www.npmjs.com/package/react-seo-optimize)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
8
+
3
9
  Simple and intuitive SEO component for React with JSON-LD schema generation and HTML injection.
4
10
 
11
+ <img width="694" height="568" alt="Screenshot 2026-01-18 at 6 29 22 PM" src="https://github.com/user-attachments/assets/7e430f06-d0da-489c-af4e-7275fd0d9506" />
12
+
5
13
  ## Features
6
14
 
7
15
  - 🚀 Simple React component for SEO meta tags
8
- - 📊 JSON-LD schema generators (Organization, ProfessionalService, Breadcrumb, WebPage)
9
- - 🔧 CLI tool to inject schemas into HTML files
10
- - 🎯 Open Graph and Twitter Card support
16
+ - 📊 JSON-LD schema generators (9 types: Organization, Article, Product, FAQ, HowTo, LocalBusiness, and more)
17
+ - 🔧 CLI tool to inject schemas into HTML files (with dry-run and backup support)
18
+ - 🎯 Open Graph and Twitter Card support (with secure URLs and article tags)
11
19
  - 💡 Intuitive API with smart defaults
20
+ - ✅ SSR/SSG ready with native DOM manipulation (no dependencies)
21
+ - 🔒 Type-safe with full TypeScript support
22
+ - 🎨 Multiple schema composition support
12
23
 
13
24
  ## Installation
14
25
 
@@ -23,9 +34,11 @@ yarn add react-seo-optimize
23
34
  **Peer Dependencies:**
24
35
 
25
36
  ```bash
26
- npm install react react-helmet
37
+ npm install react
27
38
  ```
28
39
 
40
+ > **⚠️ Breaking Change in v2.0:** The library no longer requires `react-helmet` or `react-helmet-async`. It now uses native DOM manipulation for better performance and SSR compatibility. See [Migration Guide](./MIGRATION_GUIDE.md) for details.
41
+
29
42
  ## Quick Start
30
43
 
31
44
  ### 1. Install Dependencies
@@ -35,7 +48,7 @@ npm install react react-helmet
35
48
  npm install react-seo-optimize
36
49
 
37
50
  # Install peer dependencies
38
- npm install react react-helmet
51
+ npm install react
39
52
  ```
40
53
 
41
54
  ### 2. Basic Usage in Your React Component
@@ -91,6 +104,8 @@ const AboutPage = () => {
91
104
 
92
105
  ### Step 2: Add Open Graph Tags for Social Media
93
106
 
107
+ ![unnamed](https://github.com/user-attachments/assets/de7a3720-ed5f-489b-8437-48ee758aa095)
108
+
94
109
  Enhance your social media sharing with Open Graph tags:
95
110
 
96
111
  ```jsx
@@ -107,6 +122,8 @@ Enhance your social media sharing with Open Graph tags:
107
122
 
108
123
  ### Step 3: Add Twitter Card Tags
109
124
 
125
+ <img width="585" height="537" alt="card tag" src="https://github.com/user-attachments/assets/7481749a-9a95-4f25-8236-99fcca0a9734" />
126
+
110
127
  Optimize how your links appear on Twitter:
111
128
 
112
129
  ```jsx
@@ -239,7 +256,20 @@ For static HTML files or global organization schema, use the CLI tool:
239
256
  2. Run the CLI command:
240
257
 
241
258
  ```bash
259
+ # Basic usage
242
260
  npx react-seo-generate-schema
261
+
262
+ # Preview changes without modifying files
263
+ npx react-seo-generate-schema --dry-run
264
+
265
+ # Create backup before modifying
266
+ npx react-seo-generate-schema --backup
267
+
268
+ # Custom paths
269
+ npx react-seo-generate-schema --config ./config.json --html ./public/index.html
270
+
271
+ # Help
272
+ npx react-seo-generate-schema --help
243
273
  ```
244
274
 
245
275
  Or add to your `package.json` scripts:
@@ -292,7 +322,17 @@ const ArticlePage = ({ article }) => {
292
322
  twitterImage={article.featuredImage}
293
323
  robots={article.published ? 'index, follow' : 'noindex, nofollow'}
294
324
  author={article.author.name}
295
- schema={orgSchema}
325
+ htmlLang="en"
326
+ themeColor="#ffffff"
327
+ twitterSite="@mycompany"
328
+ twitterCreator={article.author.twitter}
329
+ ogImageSecureUrl={article.featuredImage}
330
+ articlePublishedTime={article.publishedAt}
331
+ articleModifiedTime={article.updatedAt}
332
+ articleAuthor={article.author.name}
333
+ articleSection="Technology"
334
+ articleTag={article.tags}
335
+ structuredData={orgSchema}
296
336
  />
297
337
 
298
338
  <article>
@@ -306,6 +346,43 @@ const ArticlePage = ({ article }) => {
306
346
 
307
347
  ## Advanced Usage
308
348
 
349
+ ### Multiple Schema Support
350
+
351
+ You can now combine multiple schemas on a single page:
352
+
353
+ ```jsx
354
+ import { SEOptimize, generateOrganizationSchema, generateBreadcrumbSchema, generateArticleSchema } from 'react-seo-optimize';
355
+
356
+ const ArticlePage = ({ article }) => {
357
+ const orgSchema = generateOrganizationSchema({
358
+ name: 'My Company',
359
+ url: 'https://example.com',
360
+ });
361
+
362
+ const breadcrumbSchema = generateBreadcrumbSchema([
363
+ { name: 'Home', url: 'https://example.com' },
364
+ { name: 'Blog', url: 'https://example.com/blog' },
365
+ { name: article.title, url: `https://example.com/blog/${article.slug}` },
366
+ ]);
367
+
368
+ const articleSchema = generateArticleSchema({
369
+ headline: article.title,
370
+ description: article.excerpt,
371
+ datePublished: article.publishedAt,
372
+ author: { '@type': 'Person', name: article.author.name },
373
+ });
374
+
375
+ return (
376
+ <SEOptimize
377
+ title={`${article.title} | My Company`}
378
+ description={article.excerpt}
379
+ canonical={`https://example.com/blog/${article.slug}`}
380
+ structuredData={[orgSchema, breadcrumbSchema, articleSchema]}
381
+ />
382
+ );
383
+ };
384
+ ```
385
+
309
386
  ### Schema Generators Reference
310
387
 
311
388
  All available schema generators:
@@ -315,7 +392,12 @@ import {
315
392
  generateOrganizationSchema,
316
393
  generateProfessionalServiceSchema,
317
394
  generateBreadcrumbSchema,
318
- generateWebPageSchema
395
+ generateWebPageSchema,
396
+ generateArticleSchema,
397
+ generateProductSchema,
398
+ generateFAQPageSchema,
399
+ generateHowToSchema,
400
+ generateLocalBusinessSchema,
319
401
  } from 'react-seo-optimize';
320
402
 
321
403
  // Organization schema
@@ -330,12 +412,61 @@ const orgSchema = generateOrganizationSchema({
330
412
  ],
331
413
  });
332
414
 
333
- // WebPage schema
334
- const webPageSchema = generateWebPageSchema({
335
- name: 'Page Title',
336
- description: 'Page description',
337
- url: 'https://example.com/page',
338
- inLanguage: 'en',
415
+ // Article schema
416
+ const articleSchema = generateArticleSchema({
417
+ headline: 'Article Title',
418
+ description: 'Article description',
419
+ datePublished: '2024-01-01T00:00:00Z',
420
+ author: { '@type': 'Person', name: 'John Doe' },
421
+ });
422
+
423
+ // Product schema
424
+ const productSchema = generateProductSchema({
425
+ name: 'Product Name',
426
+ description: 'Product description',
427
+ image: 'https://example.com/product.jpg',
428
+ offers: {
429
+ '@type': 'Offer',
430
+ price: '99.99',
431
+ priceCurrency: 'USD',
432
+ },
433
+ });
434
+
435
+ // FAQ schema
436
+ const faqSchema = generateFAQPageSchema({
437
+ mainEntity: [
438
+ {
439
+ '@type': 'Question',
440
+ name: 'What is this?',
441
+ acceptedAnswer: {
442
+ '@type': 'Answer',
443
+ text: 'This is an answer.',
444
+ },
445
+ },
446
+ ],
447
+ });
448
+
449
+ // HowTo schema
450
+ const howToSchema = generateHowToSchema({
451
+ name: 'How to Do Something',
452
+ step: [
453
+ {
454
+ '@type': 'HowToStep',
455
+ name: 'Step 1',
456
+ text: 'First, do this',
457
+ },
458
+ ],
459
+ });
460
+
461
+ // LocalBusiness schema
462
+ const localBusinessSchema = generateLocalBusinessSchema({
463
+ name: 'My Business',
464
+ address: {
465
+ '@type': 'PostalAddress',
466
+ streetAddress: '123 Main St',
467
+ addressLocality: 'City',
468
+ addressCountry: 'US',
469
+ },
339
470
  });
340
471
  ```
341
472
 
@@ -345,26 +476,72 @@ const webPageSchema = generateWebPageSchema({
345
476
  |------|------|---------|-------------|
346
477
  | `title` | string | - | Page title |
347
478
  | `description` | string | - | Meta description |
348
- | `keywords` | string | - | Meta keywords |
349
- | `canonical` | string | - | Canonical URL |
479
+ | `keywords` | string | - | Meta keywords (deprecated by search engines) |
480
+ | `canonical` | string | - | Canonical URL (must be absolute) |
350
481
  | `ogTitle` | string | `title` | Open Graph title |
351
482
  | `ogDescription` | string | `description` | Open Graph description |
352
483
  | `ogUrl` | string | `canonical` | Open Graph URL |
353
484
  | `ogImage` | string | - | Open Graph image |
485
+ | `ogImageSecureUrl` | string | - | Open Graph secure image URL |
354
486
  | `ogType` | string | `'website'` | Open Graph type |
355
487
  | `ogImageWidth` | string | - | Open Graph image width |
356
488
  | `ogImageHeight` | string | - | Open Graph image height |
357
489
  | `ogImageAlt` | string | - | Open Graph image alt |
358
490
  | `ogSiteName` | string | - | Open Graph site name |
359
491
  | `ogLocale` | string | - | Open Graph locale |
492
+ | `articlePublishedTime` | string | - | Article published time (ISO 8601) |
493
+ | `articleModifiedTime` | string | - | Article modified time (ISO 8601) |
494
+ | `articleAuthor` | string | - | Article author |
495
+ | `articleSection` | string | - | Article section |
496
+ | `articleTag` | string[] | - | Article tags |
360
497
  | `twitterCard` | string | `'summary_large_image'` | Twitter card type |
361
498
  | `twitterTitle` | string | `title` | Twitter title |
362
499
  | `twitterDescription` | string | `description` | Twitter description |
363
500
  | `twitterImage` | string | `ogImage` | Twitter image |
364
- | `schema` | object | - | JSON-LD schema object |
365
- | `robots` | string | `'index, follow'` | Robots meta tag |
501
+ | `twitterImageAlt` | string | - | Twitter image alt |
502
+ | `twitterSite` | string | - | Twitter site (@username) |
503
+ | `twitterCreator` | string | - | Twitter creator (@username) |
504
+ | `schema` | object \| object[] | - | JSON-LD schema object(s) (deprecated, use `structuredData`) |
505
+ | `structuredData` | object \| object[] | - | JSON-LD schema object(s) |
506
+ | `robots` | string | - | Robots meta tag (no default in v2.0) |
366
507
  | `author` | string | - | Author meta tag |
367
- | `...extraMeta` | object | - | Additional meta tags |
508
+ | `htmlLang` | string | - | HTML lang attribute |
509
+ | `themeColor` | string | - | Theme color for mobile browsers |
510
+ | `viewport` | string | - | Viewport meta tag |
511
+ | `charset` | string | `'UTF-8'` | Charset meta tag |
512
+ | `customMeta` | Record<string, string> | - | Additional meta tags |
513
+
514
+ > **Note:** The `schema` prop is still supported for backward compatibility, but `structuredData` is preferred. Both support single objects or arrays for multiple schemas.
515
+
516
+ ## SSR Support
517
+
518
+ For server-side rendering, use the `renderSEOTags` utility:
519
+
520
+ ```jsx
521
+ import { renderSEOTags } from 'react-seo-optimize';
522
+
523
+ // In your SSR function
524
+ const seoTags = renderSEOTags({
525
+ title: 'Page Title',
526
+ description: 'Page description',
527
+ canonical: 'https://example.com/page',
528
+ });
529
+
530
+ // Inject into your HTML template
531
+ const html = `
532
+ <!DOCTYPE html>
533
+ <html>
534
+ <head>
535
+ ${seoTags}
536
+ </head>
537
+ <body>...</body>
538
+ </html>
539
+ `;
540
+ ```
541
+
542
+ ## Migration from v1.x
543
+
544
+ If you're upgrading from v1.x, please see the [Migration Guide](./MIGRATION_GUIDE.md) for breaking changes and upgrade instructions.
368
545
 
369
546
  ## Examples
370
547
 
@@ -373,3 +550,24 @@ See `EXAMPLE_USAGE.jsx` in the package for more examples.
373
550
  ## License
374
551
 
375
552
  MIT
553
+
554
+ ## Changelog
555
+
556
+ ### v2.0.0
557
+
558
+ **Breaking Changes:**
559
+ - Removed `react-helmet` and `react-helmet-async` dependencies (now uses native DOM manipulation)
560
+ - Removed default value for `robots` prop (must be explicit)
561
+ - `canonical` URLs must now be absolute (relative URLs rejected)
562
+ - Changed `extraMeta` to `customMeta` (object instead of spread props)
563
+
564
+ **New Features:**
565
+ - Added 5 new schema generators: Article, Product, FAQPage, HowTo, LocalBusiness
566
+ - Support for multiple schemas via `structuredData` prop (array)
567
+ - Added `ogImageSecureUrl` for HTTPS images
568
+ - Added `twitterSite` and `twitterCreator` props
569
+ - Added `htmlLang`, `themeColor`, `viewport`, `charset` props
570
+ - Added article-specific meta tags (`articlePublishedTime`, `articleModifiedTime`, etc.)
571
+ - CLI now supports `--dry-run` and `--backup` flags
572
+ - Improved TypeScript types (removed `any`, added proper types)
573
+ - Better validation for canonical URLs and image dimensions
@@ -38,8 +38,87 @@ const getProjectRoot = () => {
38
38
  return fs.existsSync(indexHtmlPath) ? currentDir : process.cwd();
39
39
  };
40
40
 
41
- const configPath = process.argv[2] || path.join(getProjectRoot(), 'schema.config.json');
42
- const indexHtmlPath = process.argv[3] || path.join(getProjectRoot(), 'index.html');
41
+ const parseArgs = () => {
42
+ const args = process.argv.slice(2);
43
+ const options = {
44
+ config: null,
45
+ html: null,
46
+ dryRun: false,
47
+ backup: false,
48
+ };
49
+
50
+ for (let i = 0; i < args.length; i++) {
51
+ const arg = args[i];
52
+ if (arg === '--dry-run' || arg === '-d') {
53
+ options.dryRun = true;
54
+ } else if (arg === '--backup' || arg === '-b') {
55
+ options.backup = true;
56
+ } else if (arg === '--config' || arg === '-c') {
57
+ options.config = args[++i];
58
+ } else if (arg === '--html' || arg === '-h') {
59
+ options.html = args[++i];
60
+ } else if (arg === '--help') {
61
+ console.log(`
62
+ Usage: react-seo-generate-schema [options]
63
+
64
+ Options:
65
+ --config, -c <path> Path to schema.config.json (default: ./schema.config.json)
66
+ --html, -h <path> Path to index.html (default: ./index.html)
67
+ --dry-run, -d Preview changes without modifying files
68
+ --backup, -b Create backup before modifying index.html
69
+ --help Show this help message
70
+
71
+ Examples:
72
+ react-seo-generate-schema
73
+ react-seo-generate-schema --dry-run
74
+ react-seo-generate-schema --backup
75
+ react-seo-generate-schema --config ./config.json --html ./public/index.html
76
+ `);
77
+ process.exit(0);
78
+ } else if (!arg.startsWith('-') && !options.config) {
79
+ options.config = arg;
80
+ } else if (!arg.startsWith('-') && options.config && !options.html) {
81
+ options.html = arg;
82
+ }
83
+ }
84
+
85
+ return options;
86
+ };
87
+
88
+ const validateConfig = (config) => {
89
+ const errors = [];
90
+
91
+ if (!config.name) {
92
+ errors.push('"name" is required');
93
+ }
94
+
95
+ if (config.url && !config.url.startsWith('http://') && !config.url.startsWith('https://')) {
96
+ errors.push('"url" must be an absolute URL');
97
+ }
98
+
99
+ if (config.logo && !config.logo.startsWith('http://') && !config.logo.startsWith('https://')) {
100
+ errors.push('"logo" must be an absolute URL');
101
+ }
102
+
103
+ if (config.sameAs && !Array.isArray(config.sameAs)) {
104
+ errors.push('"sameAs" must be an array');
105
+ }
106
+
107
+ if (config.sameAs && Array.isArray(config.sameAs)) {
108
+ config.sameAs.forEach((url, index) => {
109
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
110
+ errors.push(`"sameAs[${index}]" must be an absolute URL`);
111
+ }
112
+ });
113
+ }
114
+
115
+ return errors;
116
+ };
117
+
118
+ const options = parseArgs();
119
+ const projectRoot = getProjectRoot();
120
+ const configPath = options.config || path.join(projectRoot, 'schema.config.json');
121
+ const indexHtmlPath = options.html || path.join(projectRoot, 'index.html');
43
122
 
44
123
  let config = {};
45
124
 
@@ -62,8 +141,10 @@ if (fs.existsSync(configPath)) {
62
141
  process.exit(1);
63
142
  }
64
143
 
65
- if (!config.name) {
66
- console.error('❌ "name" is required in schema.config.json');
144
+ const validationErrors = validateConfig(config);
145
+ if (validationErrors.length > 0) {
146
+ console.error('❌ Config validation errors:');
147
+ validationErrors.forEach(error => console.error(` - ${error}`));
67
148
  process.exit(1);
68
149
  }
69
150
 
@@ -76,6 +157,7 @@ try {
76
157
  }
77
158
 
78
159
  let htmlContent = fs.readFileSync(indexHtmlPath, 'utf-8');
160
+ const originalContent = htmlContent;
79
161
 
80
162
  const schemaJsonString = JSON.stringify(schema, null, 2);
81
163
  const indentedSchema = schemaJsonString
@@ -90,22 +172,50 @@ ${indentedSchema}
90
172
 
91
173
  const scriptRegex = / <!-- Structured Data for Search Engines -->\s*<script type="application\/ld\+json">[\s\S]*?<\/script>/;
92
174
 
175
+ let action = 'unchanged';
93
176
  if (scriptRegex.test(htmlContent)) {
94
177
  htmlContent = htmlContent.replace(scriptRegex, schemaScript);
95
- console.log('✅ Schema updated in index.html');
178
+ action = 'updated';
96
179
  } else {
97
180
  const headEndRegex = /(<\/head>)/;
98
181
  if (headEndRegex.test(htmlContent)) {
99
182
  htmlContent = htmlContent.replace(headEndRegex, `${schemaScript}\n $1`);
100
- console.log('✅ Schema added to index.html');
183
+ action = 'added';
101
184
  } else {
102
185
  console.error('❌ Could not find </head> tag in index.html');
103
186
  process.exit(1);
104
187
  }
105
188
  }
106
189
 
107
- fs.writeFileSync(indexHtmlPath, htmlContent, 'utf-8');
108
- console.log(`✅ Schema successfully written to ${indexHtmlPath}`);
190
+ if (options.dryRun) {
191
+ console.log('🔍 DRY RUN MODE - No files will be modified\n');
192
+ console.log('Schema that would be generated:');
193
+ console.log(JSON.stringify(schema, null, 2));
194
+ console.log('\nAction:', action === 'unchanged' ? 'No changes needed' : `Schema would be ${action}`);
195
+ if (action !== 'unchanged') {
196
+ console.log('\nDiff preview:');
197
+ const diffStart = htmlContent.indexOf('<!-- Structured Data');
198
+ const diffEnd = htmlContent.indexOf('</script>', diffStart) + 8;
199
+ if (diffStart !== -1) {
200
+ console.log(htmlContent.substring(diffStart, diffEnd));
201
+ }
202
+ }
203
+ process.exit(0);
204
+ }
205
+
206
+ if (options.backup && action !== 'unchanged') {
207
+ const backupPath = `${indexHtmlPath}.backup.${Date.now()}`;
208
+ fs.copyFileSync(indexHtmlPath, backupPath);
209
+ console.log(`📦 Backup created: ${backupPath}`);
210
+ }
211
+
212
+ if (action !== 'unchanged') {
213
+ fs.writeFileSync(indexHtmlPath, htmlContent, 'utf-8');
214
+ console.log(`✅ Schema ${action} in index.html`);
215
+ console.log(`✅ Schema successfully written to ${indexHtmlPath}`);
216
+ } else {
217
+ console.log('✅ Schema already exists and is up to date');
218
+ }
109
219
 
110
220
  } catch (error) {
111
221
  console.error(`❌ Error updating index.html: ${error.message}`);
package/package.json CHANGED
@@ -1,16 +1,10 @@
1
1
  {
2
2
  "name": "react-seo-optimize",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Simple and intuitive SEO component for React with JSON-LD schema generation and HTML injection",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
7
7
  "type": "module",
8
- "scripts": {
9
- "generate:schema": "node bin/generate-schema.js",
10
- "test": "vitest",
11
- "test:ui": "vitest --ui",
12
- "test:coverage": "vitest --coverage"
13
- },
14
8
  "bin": {
15
9
  "react-seo-generate-schema": "./bin/generate-schema.js"
16
10
  },
@@ -21,8 +15,7 @@
21
15
  "json-ld",
22
16
  "meta-tags",
23
17
  "open-graph",
24
- "twitter-cards",
25
- "react-helmet"
18
+ "twitter-cards"
26
19
  ],
27
20
  "author": "Jeremy Inoa Fortuna",
28
21
  "email": "",
@@ -30,16 +23,14 @@
30
23
  "homepage": "https://github.com/liljemery/react-seo-optimize.git",
31
24
  "license": "MIT",
32
25
  "peerDependencies": {
33
- "react": ">=16.8.0",
34
- "react-helmet": "^6.0.0"
26
+ "react": ">=16.8.0"
35
27
  },
36
28
  "devDependencies": {
37
- "@testing-library/react": "^14.1.2",
38
29
  "@testing-library/jest-dom": "^6.1.5",
30
+ "@testing-library/react": "^14.1.2",
39
31
  "@vitest/ui": "^1.1.3",
40
32
  "jsdom": "^23.0.1",
41
33
  "react": "^18.2.0",
42
- "react-helmet": "^6.1.0",
43
34
  "vitest": "^1.1.3"
44
35
  },
45
36
  "files": [
@@ -51,5 +42,12 @@
51
42
  "repository": {
52
43
  "type": "git",
53
44
  "url": "https://github.com/liljemery/react-seo-optimize.git"
45
+ },
46
+ "scripts": {
47
+ "generate:schema": "node bin/generate-schema.js",
48
+ "test": "vitest",
49
+ "test:run": "vitest --run",
50
+ "test:ui": "vitest --ui",
51
+ "test:coverage": "vitest --coverage"
54
52
  }
55
- }
53
+ }