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 +216 -18
- package/bin/generate-schema.js +118 -8
- package/package.json +12 -14
- package/src/SEOptimize.jsx +358 -39
- package/src/index.d.ts +317 -8
- package/src/index.js +12 -1
- package/src/schemaGenerators.js +218 -0
- package/src/utils/metaTags.js +62 -0
- package/src/utils/schemaValidation.js +34 -0
- package/src/utils/ssr.js +218 -0
package/README.md
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
# react-seo-optimize
|
|
2
2
|
|
|
3
|
+
[](https://github.com/liljemery/react-seo-optimize/actions)
|
|
4
|
+
[](https://www.npmjs.com/package/react-seo-optimize)
|
|
5
|
+
[](https://www.npmjs.com/package/react-seo-optimize)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](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,
|
|
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
|
|
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
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
//
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
description: '
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
| `
|
|
365
|
-
| `
|
|
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
|
-
|
|
|
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
|
package/bin/generate-schema.js
CHANGED
|
@@ -38,8 +38,87 @@ const getProjectRoot = () => {
|
|
|
38
38
|
return fs.existsSync(indexHtmlPath) ? currentDir : process.cwd();
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
const
|
|
42
|
-
const
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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": "
|
|
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
|
+
}
|