react-seo-optimize 1.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/LICENSE +28 -0
- package/README.md +375 -0
- package/bin/generate-schema.js +113 -0
- package/package.json +55 -0
- package/src/SEOptimize.jsx +72 -0
- package/src/index.d.ts +80 -0
- package/src/index.js +2 -0
- package/src/schemaGenerators.js +115 -0
- package/src/utils/validation.js +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jeremy Inoa (liljemery)
|
|
4
|
+
Email: jeremyinoa67@gmail.com
|
|
5
|
+
GitHub: https://github.com/liljemery
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
1.β β The above copyright notice and this permission notice shall be included
|
|
15
|
+
in all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
2.β β *Attribution Requirement:* Any application, service, or derivative work
|
|
18
|
+
that makes use of this Software must provide visible and accessible credit
|
|
19
|
+
to the original author, Jeremy Inoa (liljemery), and include a link to the
|
|
20
|
+
original repository: https://github.com/liljemery/fastapi-backend.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# react-seo-optimize
|
|
2
|
+
|
|
3
|
+
Simple and intuitive SEO component for React with JSON-LD schema generation and HTML injection.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- π 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
|
|
11
|
+
- π‘ Intuitive API with smart defaults
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install react-seo-optimize
|
|
17
|
+
# or
|
|
18
|
+
pnpm add react-seo-optimize
|
|
19
|
+
# or
|
|
20
|
+
yarn add react-seo-optimize
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Peer Dependencies:**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install react react-helmet
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### 1. Install Dependencies
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Install the package
|
|
35
|
+
npm install react-seo-optimize
|
|
36
|
+
|
|
37
|
+
# Install peer dependencies
|
|
38
|
+
npm install react react-helmet
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Basic Usage in Your React Component
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
import { SEOptimize } from 'react-seo-optimize';
|
|
45
|
+
|
|
46
|
+
const HomePage = () => {
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<SEOptimize
|
|
50
|
+
title="Home | My Company"
|
|
51
|
+
description="Welcome to My Company - We provide amazing services"
|
|
52
|
+
canonical="https://example.com"
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<h1>Welcome to My Company</h1>
|
|
56
|
+
{/* Your page content */}
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That's it! The component will automatically add all necessary meta tags to your page's `<head>`.
|
|
63
|
+
|
|
64
|
+
## Tutorial
|
|
65
|
+
|
|
66
|
+
### Step 1: Basic Page SEO
|
|
67
|
+
|
|
68
|
+
Start by adding the `SEOptimize` component to your pages with essential SEO information:
|
|
69
|
+
|
|
70
|
+
```jsx
|
|
71
|
+
import { SEOptimize } from 'react-seo-optimize';
|
|
72
|
+
|
|
73
|
+
const AboutPage = () => {
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<SEOptimize
|
|
77
|
+
title="About Us | My Company"
|
|
78
|
+
description="Learn more about My Company and our mission to deliver excellence."
|
|
79
|
+
keywords="about us, company, mission"
|
|
80
|
+
canonical="https://example.com/about"
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<main>
|
|
84
|
+
<h1>About Us</h1>
|
|
85
|
+
<p>Content goes here...</p>
|
|
86
|
+
</main>
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Step 2: Add Open Graph Tags for Social Media
|
|
93
|
+
|
|
94
|
+
Enhance your social media sharing with Open Graph tags:
|
|
95
|
+
|
|
96
|
+
```jsx
|
|
97
|
+
<SEOptimize
|
|
98
|
+
title="Product Page | My Company"
|
|
99
|
+
description="Check out our amazing product"
|
|
100
|
+
canonical="https://example.com/product"
|
|
101
|
+
ogImage="https://example.com/images/product-og.jpg"
|
|
102
|
+
ogImageWidth="1200"
|
|
103
|
+
ogImageHeight="630"
|
|
104
|
+
ogType="product"
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Step 3: Add Twitter Card Tags
|
|
109
|
+
|
|
110
|
+
Optimize how your links appear on Twitter:
|
|
111
|
+
|
|
112
|
+
```jsx
|
|
113
|
+
<SEOptimize
|
|
114
|
+
title="Blog Post | My Company"
|
|
115
|
+
description="Read our latest blog post"
|
|
116
|
+
canonical="https://example.com/blog/post"
|
|
117
|
+
twitterCard="summary_large_image"
|
|
118
|
+
twitterImage="https://example.com/images/blog-twitter.jpg"
|
|
119
|
+
/>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Step 4: Add JSON-LD Schema Markup
|
|
123
|
+
|
|
124
|
+
Add structured data to help search engines understand your content better.
|
|
125
|
+
|
|
126
|
+
#### Organization Schema (Homepage/Global)
|
|
127
|
+
|
|
128
|
+
```jsx
|
|
129
|
+
import { SEOptimize, generateOrganizationSchema } from 'react-seo-optimize';
|
|
130
|
+
|
|
131
|
+
const HomePage = () => {
|
|
132
|
+
const orgSchema = generateOrganizationSchema({
|
|
133
|
+
name: 'My Company',
|
|
134
|
+
url: 'https://example.com',
|
|
135
|
+
logo: 'https://example.com/logo.png',
|
|
136
|
+
description: 'Leading provider of amazing services',
|
|
137
|
+
sameAs: [
|
|
138
|
+
'https://facebook.com/mycompany',
|
|
139
|
+
'https://twitter.com/mycompany',
|
|
140
|
+
'https://linkedin.com/company/mycompany',
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
<SEOptimize
|
|
147
|
+
title="Home | My Company"
|
|
148
|
+
description="Welcome to My Company"
|
|
149
|
+
canonical="https://example.com"
|
|
150
|
+
schema={orgSchema}
|
|
151
|
+
/>
|
|
152
|
+
{/* Content */}
|
|
153
|
+
</>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Breadcrumb Schema (Navigation)
|
|
159
|
+
|
|
160
|
+
```jsx
|
|
161
|
+
import { SEOptimize, generateBreadcrumbSchema } from 'react-seo-optimize';
|
|
162
|
+
|
|
163
|
+
const ProductPage = () => {
|
|
164
|
+
const breadcrumbSchema = generateBreadcrumbSchema([
|
|
165
|
+
{ name: 'Home', url: 'https://example.com' },
|
|
166
|
+
{ name: 'Products', url: 'https://example.com/products' },
|
|
167
|
+
{ name: 'Product Name', url: 'https://example.com/products/123' },
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<>
|
|
172
|
+
<SEOptimize
|
|
173
|
+
title="Product Name | My Company"
|
|
174
|
+
description="Product description"
|
|
175
|
+
canonical="https://example.com/products/123"
|
|
176
|
+
schema={breadcrumbSchema}
|
|
177
|
+
/>
|
|
178
|
+
{/* Product content */}
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### Professional Service Schema
|
|
185
|
+
|
|
186
|
+
```jsx
|
|
187
|
+
import { SEOptimize, generateProfessionalServiceSchema } from 'react-seo-optimize';
|
|
188
|
+
|
|
189
|
+
const ServicesPage = () => {
|
|
190
|
+
const serviceSchema = generateProfessionalServiceSchema({
|
|
191
|
+
name: 'Professional Services',
|
|
192
|
+
description: 'We offer professional consulting services',
|
|
193
|
+
url: 'https://example.com/services',
|
|
194
|
+
areaServed: {
|
|
195
|
+
'@type': 'Country',
|
|
196
|
+
name: 'United States',
|
|
197
|
+
},
|
|
198
|
+
serviceType: 'Consulting',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<>
|
|
203
|
+
<SEOptimize
|
|
204
|
+
title="Our Services | My Company"
|
|
205
|
+
description="Professional services we offer"
|
|
206
|
+
canonical="https://example.com/services"
|
|
207
|
+
schema={serviceSchema}
|
|
208
|
+
/>
|
|
209
|
+
{/* Services content */}
|
|
210
|
+
</>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Step 5: Use CLI to Inject Global Schema
|
|
216
|
+
|
|
217
|
+
For static HTML files or global organization schema, use the CLI tool:
|
|
218
|
+
|
|
219
|
+
1. Create `schema.config.json` in your project root:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"name": "My Organization",
|
|
224
|
+
"url": "https://example.com",
|
|
225
|
+
"logo": "https://example.com/logo.svg",
|
|
226
|
+
"description": "Organization description",
|
|
227
|
+
"address": {
|
|
228
|
+
"@type": "PostalAddress",
|
|
229
|
+
"addressCountry": "US",
|
|
230
|
+
"addressLocality": "City"
|
|
231
|
+
},
|
|
232
|
+
"sameAs": [
|
|
233
|
+
"https://facebook.com/myorg",
|
|
234
|
+
"https://twitter.com/myorg"
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
2. Run the CLI command:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npx react-seo-generate-schema
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Or add to your `package.json` scripts:
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"scripts": {
|
|
250
|
+
"generate:schema": "react-seo-generate-schema"
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Then run:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
npm run generate:schema
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
This will automatically inject the JSON-LD schema into your `index.html` file.
|
|
262
|
+
|
|
263
|
+
## Complete Example
|
|
264
|
+
|
|
265
|
+
Here's a complete example combining multiple features:
|
|
266
|
+
|
|
267
|
+
```jsx
|
|
268
|
+
import { SEOptimize, generateOrganizationSchema } from 'react-seo-optimize';
|
|
269
|
+
|
|
270
|
+
const ArticlePage = ({ article }) => {
|
|
271
|
+
// Generate schema for the organization
|
|
272
|
+
const orgSchema = generateOrganizationSchema({
|
|
273
|
+
name: 'My Company',
|
|
274
|
+
url: 'https://example.com',
|
|
275
|
+
logo: 'https://example.com/logo.png',
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<>
|
|
280
|
+
<SEOptimize
|
|
281
|
+
title={`${article.title} | My Company Blog`}
|
|
282
|
+
description={article.excerpt}
|
|
283
|
+
keywords={article.tags.join(', ')}
|
|
284
|
+
canonical={`https://example.com/blog/${article.slug}`}
|
|
285
|
+
ogTitle={article.title}
|
|
286
|
+
ogDescription={article.excerpt}
|
|
287
|
+
ogImage={article.featuredImage}
|
|
288
|
+
ogType="article"
|
|
289
|
+
ogImageWidth="1200"
|
|
290
|
+
ogImageHeight="630"
|
|
291
|
+
twitterCard="summary_large_image"
|
|
292
|
+
twitterImage={article.featuredImage}
|
|
293
|
+
robots={article.published ? 'index, follow' : 'noindex, nofollow'}
|
|
294
|
+
author={article.author.name}
|
|
295
|
+
schema={orgSchema}
|
|
296
|
+
/>
|
|
297
|
+
|
|
298
|
+
<article>
|
|
299
|
+
<h1>{article.title}</h1>
|
|
300
|
+
<div dangerouslySetInnerHTML={{ __html: article.content }} />
|
|
301
|
+
</article>
|
|
302
|
+
</>
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Advanced Usage
|
|
308
|
+
|
|
309
|
+
### Schema Generators Reference
|
|
310
|
+
|
|
311
|
+
All available schema generators:
|
|
312
|
+
|
|
313
|
+
```jsx
|
|
314
|
+
import {
|
|
315
|
+
generateOrganizationSchema,
|
|
316
|
+
generateProfessionalServiceSchema,
|
|
317
|
+
generateBreadcrumbSchema,
|
|
318
|
+
generateWebPageSchema
|
|
319
|
+
} from 'react-seo-optimize';
|
|
320
|
+
|
|
321
|
+
// Organization schema
|
|
322
|
+
const orgSchema = generateOrganizationSchema({
|
|
323
|
+
name: 'My Company',
|
|
324
|
+
url: 'https://example.com',
|
|
325
|
+
logo: 'https://example.com/logo.svg',
|
|
326
|
+
description: 'Company description',
|
|
327
|
+
sameAs: [
|
|
328
|
+
'https://facebook.com/mycompany',
|
|
329
|
+
'https://twitter.com/mycompany',
|
|
330
|
+
],
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// WebPage schema
|
|
334
|
+
const webPageSchema = generateWebPageSchema({
|
|
335
|
+
name: 'Page Title',
|
|
336
|
+
description: 'Page description',
|
|
337
|
+
url: 'https://example.com/page',
|
|
338
|
+
inLanguage: 'en',
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Component Props
|
|
343
|
+
|
|
344
|
+
| Prop | Type | Default | Description |
|
|
345
|
+
|------|------|---------|-------------|
|
|
346
|
+
| `title` | string | - | Page title |
|
|
347
|
+
| `description` | string | - | Meta description |
|
|
348
|
+
| `keywords` | string | - | Meta keywords |
|
|
349
|
+
| `canonical` | string | - | Canonical URL |
|
|
350
|
+
| `ogTitle` | string | `title` | Open Graph title |
|
|
351
|
+
| `ogDescription` | string | `description` | Open Graph description |
|
|
352
|
+
| `ogUrl` | string | `canonical` | Open Graph URL |
|
|
353
|
+
| `ogImage` | string | - | Open Graph image |
|
|
354
|
+
| `ogType` | string | `'website'` | Open Graph type |
|
|
355
|
+
| `ogImageWidth` | string | - | Open Graph image width |
|
|
356
|
+
| `ogImageHeight` | string | - | Open Graph image height |
|
|
357
|
+
| `ogImageAlt` | string | - | Open Graph image alt |
|
|
358
|
+
| `ogSiteName` | string | - | Open Graph site name |
|
|
359
|
+
| `ogLocale` | string | - | Open Graph locale |
|
|
360
|
+
| `twitterCard` | string | `'summary_large_image'` | Twitter card type |
|
|
361
|
+
| `twitterTitle` | string | `title` | Twitter title |
|
|
362
|
+
| `twitterDescription` | string | `description` | Twitter description |
|
|
363
|
+
| `twitterImage` | string | `ogImage` | Twitter image |
|
|
364
|
+
| `schema` | object | - | JSON-LD schema object |
|
|
365
|
+
| `robots` | string | `'index, follow'` | Robots meta tag |
|
|
366
|
+
| `author` | string | - | Author meta tag |
|
|
367
|
+
| `...extraMeta` | object | - | Additional meta tags |
|
|
368
|
+
|
|
369
|
+
## Examples
|
|
370
|
+
|
|
371
|
+
See `EXAMPLE_USAGE.jsx` in the package for more examples.
|
|
372
|
+
|
|
373
|
+
## License
|
|
374
|
+
|
|
375
|
+
MIT
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const generateOrganizationSchema = (config) => {
|
|
11
|
+
const schema = {
|
|
12
|
+
'@context': 'https://schema.org',
|
|
13
|
+
'@type': 'Organization',
|
|
14
|
+
name: config.name,
|
|
15
|
+
...(config.alternateName && { alternateName: config.alternateName }),
|
|
16
|
+
...(config.url && { url: config.url }),
|
|
17
|
+
...(config.logo && { logo: config.logo }),
|
|
18
|
+
...(config.description && { description: config.description }),
|
|
19
|
+
...(config.address && { address: config.address }),
|
|
20
|
+
...(config.contactPoint && { contactPoint: config.contactPoint }),
|
|
21
|
+
...(config.sameAs && config.sameAs.length > 0 && { sameAs: config.sameAs }),
|
|
22
|
+
...(config.areaServed && { areaServed: config.areaServed }),
|
|
23
|
+
...(config.hasOfferCatalog && { hasOfferCatalog: config.hasOfferCatalog }),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return schema;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getProjectRoot = () => {
|
|
30
|
+
let currentDir = process.cwd();
|
|
31
|
+
let indexHtmlPath = path.join(currentDir, 'index.html');
|
|
32
|
+
|
|
33
|
+
while (!fs.existsSync(indexHtmlPath) && currentDir !== path.dirname(currentDir)) {
|
|
34
|
+
currentDir = path.dirname(currentDir);
|
|
35
|
+
indexHtmlPath = path.join(currentDir, 'index.html');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return fs.existsSync(indexHtmlPath) ? currentDir : process.cwd();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const configPath = process.argv[2] || path.join(getProjectRoot(), 'schema.config.json');
|
|
42
|
+
const indexHtmlPath = process.argv[3] || path.join(getProjectRoot(), 'index.html');
|
|
43
|
+
|
|
44
|
+
let config = {};
|
|
45
|
+
|
|
46
|
+
if (fs.existsSync(configPath)) {
|
|
47
|
+
try {
|
|
48
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`β Error reading config file: ${error.message}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
console.log(`β οΈ Config file not found at ${configPath}`);
|
|
55
|
+
console.log('Please create a schema.config.json file with your organization details.');
|
|
56
|
+
console.log('Example:');
|
|
57
|
+
console.log(JSON.stringify({
|
|
58
|
+
name: 'Your Organization',
|
|
59
|
+
url: 'https://example.com',
|
|
60
|
+
description: 'Your description',
|
|
61
|
+
}, null, 2));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!config.name) {
|
|
66
|
+
console.error('β "name" is required in schema.config.json');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const schema = generateOrganizationSchema(config);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
if (!fs.existsSync(indexHtmlPath)) {
|
|
74
|
+
console.error(`β index.html not found at ${indexHtmlPath}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let htmlContent = fs.readFileSync(indexHtmlPath, 'utf-8');
|
|
79
|
+
|
|
80
|
+
const schemaJsonString = JSON.stringify(schema, null, 2);
|
|
81
|
+
const indentedSchema = schemaJsonString
|
|
82
|
+
.split('\n')
|
|
83
|
+
.map((line) => ` ${line}`)
|
|
84
|
+
.join('\n');
|
|
85
|
+
|
|
86
|
+
const schemaScript = ` <!-- Structured Data for Search Engines -->
|
|
87
|
+
<script type="application/ld+json">
|
|
88
|
+
${indentedSchema}
|
|
89
|
+
</script>`;
|
|
90
|
+
|
|
91
|
+
const scriptRegex = / <!-- Structured Data for Search Engines -->\s*<script type="application\/ld\+json">[\s\S]*?<\/script>/;
|
|
92
|
+
|
|
93
|
+
if (scriptRegex.test(htmlContent)) {
|
|
94
|
+
htmlContent = htmlContent.replace(scriptRegex, schemaScript);
|
|
95
|
+
console.log('β
Schema updated in index.html');
|
|
96
|
+
} else {
|
|
97
|
+
const headEndRegex = /(<\/head>)/;
|
|
98
|
+
if (headEndRegex.test(htmlContent)) {
|
|
99
|
+
htmlContent = htmlContent.replace(headEndRegex, `${schemaScript}\n $1`);
|
|
100
|
+
console.log('β
Schema added to index.html');
|
|
101
|
+
} else {
|
|
102
|
+
console.error('β Could not find </head> tag in index.html');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fs.writeFileSync(indexHtmlPath, htmlContent, 'utf-8');
|
|
108
|
+
console.log(`β
Schema successfully written to ${indexHtmlPath}`);
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(`β Error updating index.html: ${error.message}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-seo-optimize",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Simple and intuitive SEO component for React with JSON-LD schema generation and HTML injection",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
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
|
+
"bin": {
|
|
15
|
+
"react-seo-generate-schema": "./bin/generate-schema.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"react",
|
|
19
|
+
"seo",
|
|
20
|
+
"schema.org",
|
|
21
|
+
"json-ld",
|
|
22
|
+
"meta-tags",
|
|
23
|
+
"open-graph",
|
|
24
|
+
"twitter-cards",
|
|
25
|
+
"react-helmet"
|
|
26
|
+
],
|
|
27
|
+
"author": "Jeremy Inoa Fortuna",
|
|
28
|
+
"email": "",
|
|
29
|
+
"url": "https://github.com/liljemery/react-seo-optimize.git",
|
|
30
|
+
"homepage": "https://github.com/liljemery/react-seo-optimize.git",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": ">=16.8.0",
|
|
34
|
+
"react-helmet": "^6.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@testing-library/react": "^14.1.2",
|
|
38
|
+
"@testing-library/jest-dom": "^6.1.5",
|
|
39
|
+
"@vitest/ui": "^1.1.3",
|
|
40
|
+
"jsdom": "^23.0.1",
|
|
41
|
+
"react": "^18.2.0",
|
|
42
|
+
"react-helmet": "^6.1.0",
|
|
43
|
+
"vitest": "^1.1.3"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"src/",
|
|
47
|
+
"bin/",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
],
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/liljemery/react-seo-optimize.git"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import { Helmet } from 'react-helmet';
|
|
3
|
+
|
|
4
|
+
const SEOptimize = ({
|
|
5
|
+
title,
|
|
6
|
+
description,
|
|
7
|
+
keywords,
|
|
8
|
+
canonical,
|
|
9
|
+
ogTitle,
|
|
10
|
+
ogDescription,
|
|
11
|
+
ogUrl,
|
|
12
|
+
ogImage,
|
|
13
|
+
ogType = 'website',
|
|
14
|
+
ogImageWidth,
|
|
15
|
+
ogImageHeight,
|
|
16
|
+
ogImageAlt,
|
|
17
|
+
ogSiteName,
|
|
18
|
+
ogLocale,
|
|
19
|
+
twitterCard = 'summary_large_image',
|
|
20
|
+
twitterTitle,
|
|
21
|
+
twitterDescription,
|
|
22
|
+
twitterImage,
|
|
23
|
+
twitterImageAlt,
|
|
24
|
+
schema,
|
|
25
|
+
robots = 'index, follow',
|
|
26
|
+
author,
|
|
27
|
+
...extraMeta
|
|
28
|
+
}) => {
|
|
29
|
+
const finalOgTitle = ogTitle || title;
|
|
30
|
+
const finalOgDescription = ogDescription || description;
|
|
31
|
+
const finalOgUrl = ogUrl || canonical;
|
|
32
|
+
const finalTwitterTitle = twitterTitle || title;
|
|
33
|
+
const finalTwitterDescription = twitterDescription || description;
|
|
34
|
+
const finalTwitterImage = twitterImage || ogImage;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Helmet>
|
|
38
|
+
{title && <title>{title}</title>}
|
|
39
|
+
{description && <meta name="description" content={description} />}
|
|
40
|
+
{keywords && <meta name="keywords" content={keywords} />}
|
|
41
|
+
{canonical && <link rel="canonical" href={canonical} />}
|
|
42
|
+
{robots && <meta name="robots" content={robots} />}
|
|
43
|
+
{author && <meta name="author" content={author} />}
|
|
44
|
+
|
|
45
|
+
{finalOgTitle && <meta property="og:title" content={finalOgTitle} />}
|
|
46
|
+
{finalOgDescription && <meta property="og:description" content={finalOgDescription} />}
|
|
47
|
+
{finalOgUrl && <meta property="og:url" content={finalOgUrl} />}
|
|
48
|
+
{ogType && <meta property="og:type" content={ogType} />}
|
|
49
|
+
{ogImage && <meta property="og:image" content={ogImage} />}
|
|
50
|
+
{ogImageWidth && <meta property="og:image:width" content={ogImageWidth} />}
|
|
51
|
+
{ogImageHeight && <meta property="og:image:height" content={ogImageHeight} />}
|
|
52
|
+
{ogImageAlt && <meta property="og:image:alt" content={ogImageAlt} />}
|
|
53
|
+
{ogSiteName && <meta property="og:site_name" content={ogSiteName} />}
|
|
54
|
+
{ogLocale && <meta property="og:locale" content={ogLocale} />}
|
|
55
|
+
|
|
56
|
+
{twitterCard && <meta name="twitter:card" content={twitterCard} />}
|
|
57
|
+
{finalTwitterTitle && <meta name="twitter:title" content={finalTwitterTitle} />}
|
|
58
|
+
{finalTwitterDescription && <meta name="twitter:description" content={finalTwitterDescription} />}
|
|
59
|
+
{finalOgUrl && <meta name="twitter:url" content={finalOgUrl} />}
|
|
60
|
+
{finalTwitterImage && <meta name="twitter:image" content={finalTwitterImage} />}
|
|
61
|
+
{twitterImageAlt && <meta name="twitter:image:alt" content={twitterImageAlt} />}
|
|
62
|
+
|
|
63
|
+
{schema && <script type="application/ld+json">{JSON.stringify(schema)}</script>}
|
|
64
|
+
|
|
65
|
+
{Object.entries(extraMeta).map(([key, value]) => (
|
|
66
|
+
<meta key={key} name={key} content={value} />
|
|
67
|
+
))}
|
|
68
|
+
</Helmet>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default memo(SEOptimize);
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SEOptimizeProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
keywords?: string;
|
|
7
|
+
canonical?: string;
|
|
8
|
+
ogTitle?: string;
|
|
9
|
+
ogDescription?: string;
|
|
10
|
+
ogUrl?: string;
|
|
11
|
+
ogImage?: string;
|
|
12
|
+
ogType?: string;
|
|
13
|
+
ogImageWidth?: string;
|
|
14
|
+
ogImageHeight?: string;
|
|
15
|
+
ogImageAlt?: string;
|
|
16
|
+
ogSiteName?: string;
|
|
17
|
+
ogLocale?: string;
|
|
18
|
+
twitterCard?: string;
|
|
19
|
+
twitterTitle?: string;
|
|
20
|
+
twitterDescription?: string;
|
|
21
|
+
twitterImage?: string;
|
|
22
|
+
twitterImageAlt?: string;
|
|
23
|
+
schema?: object;
|
|
24
|
+
robots?: string;
|
|
25
|
+
author?: string;
|
|
26
|
+
[key: string]: any;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export declare const SEOptimize: ComponentType<SEOptimizeProps>;
|
|
30
|
+
|
|
31
|
+
export interface OrganizationSchemaConfig {
|
|
32
|
+
name: string;
|
|
33
|
+
alternateName?: string;
|
|
34
|
+
url?: string;
|
|
35
|
+
logo?: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
address?: object;
|
|
38
|
+
contactPoint?: object;
|
|
39
|
+
sameAs?: string[];
|
|
40
|
+
areaServed?: object;
|
|
41
|
+
hasOfferCatalog?: object;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ProfessionalServiceSchemaConfig {
|
|
45
|
+
name?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
url?: string;
|
|
48
|
+
areaServed?: object;
|
|
49
|
+
serviceType?: string;
|
|
50
|
+
provider?: object;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BreadcrumbItem {
|
|
54
|
+
name: string;
|
|
55
|
+
url: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface WebPageSchemaConfig {
|
|
59
|
+
name?: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
url?: string;
|
|
62
|
+
inLanguage?: string;
|
|
63
|
+
isPartOf?: object;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export declare function generateOrganizationSchema(
|
|
67
|
+
config: OrganizationSchemaConfig
|
|
68
|
+
): object;
|
|
69
|
+
|
|
70
|
+
export declare function generateProfessionalServiceSchema(
|
|
71
|
+
config: ProfessionalServiceSchemaConfig
|
|
72
|
+
): object;
|
|
73
|
+
|
|
74
|
+
export declare function generateBreadcrumbSchema(
|
|
75
|
+
items: BreadcrumbItem[]
|
|
76
|
+
): object;
|
|
77
|
+
|
|
78
|
+
export declare function generateWebPageSchema(
|
|
79
|
+
config: WebPageSchemaConfig
|
|
80
|
+
): object;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { validateRequired, validateUrl, validateArray } from './utils/validation.js';
|
|
2
|
+
|
|
3
|
+
export const generateOrganizationSchema = ({
|
|
4
|
+
name,
|
|
5
|
+
alternateName,
|
|
6
|
+
url,
|
|
7
|
+
logo,
|
|
8
|
+
description,
|
|
9
|
+
address,
|
|
10
|
+
contactPoint,
|
|
11
|
+
sameAs = [],
|
|
12
|
+
areaServed,
|
|
13
|
+
hasOfferCatalog,
|
|
14
|
+
}) => {
|
|
15
|
+
validateRequired(name, 'name');
|
|
16
|
+
|
|
17
|
+
if (url) validateUrl(url, 'url');
|
|
18
|
+
if (logo) validateUrl(logo, 'logo');
|
|
19
|
+
validateArray(sameAs, 'sameAs');
|
|
20
|
+
|
|
21
|
+
const schema = {
|
|
22
|
+
'@context': 'https://schema.org',
|
|
23
|
+
'@type': 'Organization',
|
|
24
|
+
name,
|
|
25
|
+
...(alternateName && { alternateName }),
|
|
26
|
+
...(url && { url }),
|
|
27
|
+
...(logo && { logo }),
|
|
28
|
+
...(description && { description }),
|
|
29
|
+
...(address && { address }),
|
|
30
|
+
...(contactPoint && { contactPoint }),
|
|
31
|
+
...(sameAs.length > 0 && { sameAs }),
|
|
32
|
+
...(areaServed && { areaServed }),
|
|
33
|
+
...(hasOfferCatalog && { hasOfferCatalog }),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return schema;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const generateProfessionalServiceSchema = ({
|
|
40
|
+
name,
|
|
41
|
+
description,
|
|
42
|
+
url,
|
|
43
|
+
areaServed,
|
|
44
|
+
serviceType,
|
|
45
|
+
provider,
|
|
46
|
+
}) => {
|
|
47
|
+
if (url) validateUrl(url, 'url');
|
|
48
|
+
|
|
49
|
+
const schema = {
|
|
50
|
+
'@context': 'https://schema.org',
|
|
51
|
+
'@type': 'ProfessionalService',
|
|
52
|
+
...(name && { name }),
|
|
53
|
+
...(description && { description }),
|
|
54
|
+
...(url && { url }),
|
|
55
|
+
...(areaServed && { areaServed }),
|
|
56
|
+
...(serviceType && { serviceType }),
|
|
57
|
+
...(provider && { provider }),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return schema;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const generateBreadcrumbSchema = (items) => {
|
|
64
|
+
validateRequired(items, 'items');
|
|
65
|
+
validateArray(items, 'items');
|
|
66
|
+
|
|
67
|
+
if (items.length === 0) {
|
|
68
|
+
throw new Error('Breadcrumb items array cannot be empty.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
items.forEach((item, index) => {
|
|
72
|
+
if (!item.name) {
|
|
73
|
+
throw new Error(`Breadcrumb item at index ${index} is missing required field "name".`);
|
|
74
|
+
}
|
|
75
|
+
if (!item.url) {
|
|
76
|
+
throw new Error(`Breadcrumb item at index ${index} is missing required field "url".`);
|
|
77
|
+
}
|
|
78
|
+
validateUrl(item.url, `items[${index}].url`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const schema = {
|
|
82
|
+
'@context': 'https://schema.org',
|
|
83
|
+
'@type': 'BreadcrumbList',
|
|
84
|
+
itemListElement: items.map((item, index) => ({
|
|
85
|
+
'@type': 'ListItem',
|
|
86
|
+
position: index + 1,
|
|
87
|
+
name: item.name,
|
|
88
|
+
item: item.url,
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return schema;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const generateWebPageSchema = ({
|
|
96
|
+
name,
|
|
97
|
+
description,
|
|
98
|
+
url,
|
|
99
|
+
inLanguage = 'es',
|
|
100
|
+
isPartOf,
|
|
101
|
+
}) => {
|
|
102
|
+
if (url) validateUrl(url, 'url');
|
|
103
|
+
|
|
104
|
+
const schema = {
|
|
105
|
+
'@context': 'https://schema.org',
|
|
106
|
+
'@type': 'WebPage',
|
|
107
|
+
...(name && { name }),
|
|
108
|
+
...(description && { description }),
|
|
109
|
+
...(url && { url }),
|
|
110
|
+
...(inLanguage && { inLanguage }),
|
|
111
|
+
...(isPartOf && { isPartOf }),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return schema;
|
|
115
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const isValidUrl = (url) => {
|
|
2
|
+
if (typeof url !== 'string') return false;
|
|
3
|
+
try {
|
|
4
|
+
new URL(url);
|
|
5
|
+
return true;
|
|
6
|
+
} catch {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const validateUrl = (url, fieldName = 'url') => {
|
|
12
|
+
if (url && !isValidUrl(url)) {
|
|
13
|
+
throw new Error(`Invalid ${fieldName}: "${url}". Must be a valid URL.`);
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const validateRequired = (value, fieldName) => {
|
|
19
|
+
if (!value) {
|
|
20
|
+
throw new Error(`Required field "${fieldName}" is missing.`);
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const validateArray = (value, fieldName) => {
|
|
26
|
+
if (value && !Array.isArray(value)) {
|
|
27
|
+
throw new Error(`"${fieldName}" must be an array.`);
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
};
|