seox 0.1.0 → 1.1.1
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 +166 -5
- package/dist/cli.cjs +56 -39
- package/dist/cli.js +56 -39
- package/dist/next.cjs +1 -1
- package/dist/next.d.cts +43 -3
- package/dist/next.d.ts +43 -3
- package/dist/next.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Seox is an **open source tool** that centralizes and automates **SEO management** (meta tags, Open Graph, JSON-LD...) in **Next.js** projects using the **App Router**.
|
|
6
6
|
It combines **TypeScript-typed configuration**, **automatic metadata injection**, and an **intuitive CLI** to guide developers.
|
|
7
7
|
|
|
8
|
-
[](https://www.npmjs.com/package/seox)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
11
|
|
|
@@ -130,10 +130,24 @@ After running `bunx seox configure`, your files are automatically updated:
|
|
|
130
130
|
```tsx
|
|
131
131
|
// app/layout.tsx (automatically generated)
|
|
132
132
|
import { seoConfig } from "@/lib/seo";
|
|
133
|
+
import { JsonLd } from "seox/next";
|
|
133
134
|
|
|
134
135
|
export const metadata = seoConfig.configToMetadata();
|
|
136
|
+
|
|
137
|
+
export default function RootLayout({ children }) {
|
|
138
|
+
return (
|
|
139
|
+
<html lang="en">
|
|
140
|
+
<head>
|
|
141
|
+
<JsonLd seo={seoConfig} />
|
|
142
|
+
</head>
|
|
143
|
+
<body>{children}</body>
|
|
144
|
+
</html>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
135
147
|
```
|
|
136
148
|
|
|
149
|
+
> 💡 The `<JsonLd />` component automatically injects all JSON-LD structured data configured in your `seoConfig`.
|
|
150
|
+
|
|
137
151
|
#### Page-specific Customization
|
|
138
152
|
|
|
139
153
|
```tsx
|
|
@@ -189,6 +203,134 @@ Seox merges these fields with the global configuration.
|
|
|
189
203
|
|
|
190
204
|
---
|
|
191
205
|
|
|
206
|
+
## 🏗️ JSON-LD Structured Data
|
|
207
|
+
|
|
208
|
+
Seox provides full support for **JSON-LD structured data** to improve your SEO with rich snippets in Google search results.
|
|
209
|
+
|
|
210
|
+
### Configuration
|
|
211
|
+
|
|
212
|
+
Add your JSON-LD schemas in your `seo.ts` configuration:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { Seox } from "seox/next";
|
|
216
|
+
|
|
217
|
+
export const seoConfig = new Seox({
|
|
218
|
+
name: "My Site",
|
|
219
|
+
url: "https://mysite.com",
|
|
220
|
+
// ... other config
|
|
221
|
+
jsonld: [
|
|
222
|
+
{
|
|
223
|
+
"@context": "https://schema.org",
|
|
224
|
+
"@type": "Organization",
|
|
225
|
+
name: "My Company",
|
|
226
|
+
url: "https://mysite.com",
|
|
227
|
+
logo: "https://mysite.com/logo.png",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"@context": "https://schema.org",
|
|
231
|
+
"@type": "LocalBusiness",
|
|
232
|
+
name: "My Business",
|
|
233
|
+
address: {
|
|
234
|
+
"@type": "PostalAddress",
|
|
235
|
+
addressLocality: "Paris",
|
|
236
|
+
addressCountry: "FR",
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"@context": "https://schema.org",
|
|
241
|
+
"@type": "FAQPage",
|
|
242
|
+
mainEntity: [
|
|
243
|
+
{
|
|
244
|
+
"@type": "Question",
|
|
245
|
+
name: "What is your service?",
|
|
246
|
+
acceptedAnswer: {
|
|
247
|
+
"@type": "Answer",
|
|
248
|
+
text: "We provide amazing services...",
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Usage with `<JsonLd />` Component
|
|
258
|
+
|
|
259
|
+
The `<JsonLd />` component automatically renders all your JSON-LD schemas as `<script type="application/ld+json">` tags:
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
// app/layout.tsx
|
|
263
|
+
import { seoConfig } from "@/lib/seo";
|
|
264
|
+
import { JsonLd } from "seox/next";
|
|
265
|
+
|
|
266
|
+
export default function RootLayout({ children }) {
|
|
267
|
+
return (
|
|
268
|
+
<html lang="en">
|
|
269
|
+
<head>
|
|
270
|
+
<JsonLd seo={seoConfig} />
|
|
271
|
+
</head>
|
|
272
|
+
<body>{children}</body>
|
|
273
|
+
</html>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Adding Page-specific Schemas
|
|
279
|
+
|
|
280
|
+
You can add additional schemas for specific pages:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
// app/product/[id]/page.tsx
|
|
284
|
+
import { seoConfig } from "@/lib/seo";
|
|
285
|
+
import { JsonLd } from "seox/next";
|
|
286
|
+
|
|
287
|
+
export default function ProductPage() {
|
|
288
|
+
const productSchema = {
|
|
289
|
+
"@context": "https://schema.org",
|
|
290
|
+
"@type": "Product",
|
|
291
|
+
name: "Amazing Product",
|
|
292
|
+
price: "99.99",
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<>
|
|
297
|
+
<JsonLd seo={seoConfig} additionalSchemas={[productSchema]} />
|
|
298
|
+
{/* Page content */}
|
|
299
|
+
</>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Programmatic Access
|
|
305
|
+
|
|
306
|
+
You can also access JSON-LD data programmatically:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
// Get JSON-LD as objects
|
|
310
|
+
const schemas = seoConfig.getJsonLd();
|
|
311
|
+
|
|
312
|
+
// Get JSON-LD as stringified JSON (ready for injection)
|
|
313
|
+
const jsonStrings = seoConfig.getJsonLdStrings();
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Supported Schema Types
|
|
317
|
+
|
|
318
|
+
Seox supports all [Schema.org](https://schema.org) types:
|
|
319
|
+
|
|
320
|
+
| Schema Type | Use Case |
|
|
321
|
+
|-------------|----------|
|
|
322
|
+
| `Organization` | Company information |
|
|
323
|
+
| `LocalBusiness` | Local business with address, hours |
|
|
324
|
+
| `FAQPage` | FAQ sections for rich snippets |
|
|
325
|
+
| `Product` | E-commerce products |
|
|
326
|
+
| `Article` | Blog posts and articles |
|
|
327
|
+
| `BreadcrumbList` | Navigation breadcrumbs |
|
|
328
|
+
| `WebSite` | Site-wide search box |
|
|
329
|
+
| `Person` | Author profiles |
|
|
330
|
+
| `Review` / `AggregateRating` | Reviews and ratings |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
192
334
|
## 🧰 Built-in CLI
|
|
193
335
|
|
|
194
336
|
Seox provides an intuitive CLI:
|
|
@@ -240,8 +382,26 @@ Centralized SEO configuration with complete TypeScript typing.
|
|
|
240
382
|
Method that generates Next.js metadata from your configuration.
|
|
241
383
|
Accepts optional overrides to customize per page.
|
|
242
384
|
|
|
243
|
-
### `
|
|
244
|
-
|
|
385
|
+
### `getJsonLd(additionalSchemas?)`
|
|
386
|
+
Returns the JSON-LD schemas as an array of objects.
|
|
387
|
+
Accepts optional additional schemas to merge with config.
|
|
388
|
+
|
|
389
|
+
### `getJsonLdStrings(additionalSchemas?)`
|
|
390
|
+
Returns the JSON-LD schemas as stringified JSON strings, ready for injection.
|
|
391
|
+
|
|
392
|
+
### `<JsonLd />` Component
|
|
393
|
+
React component to inject JSON-LD structured data into the page `<head>`.
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
import { JsonLd } from "seox/next";
|
|
397
|
+
|
|
398
|
+
<JsonLd seo={seoConfig} additionalSchemas={[...]} />
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
| Prop | Type | Description |
|
|
402
|
+
|------|------|-------------|
|
|
403
|
+
| `seo` | `Seox` | Your Seox configuration instance |
|
|
404
|
+
| `additionalSchemas` | `SEOJsonLd[]` | Optional additional schemas to include |
|
|
245
405
|
|
|
246
406
|
---
|
|
247
407
|
|
|
@@ -259,7 +419,8 @@ Direct usage in your Next.js files to generate metadata.
|
|
|
259
419
|
| Multilingual support (`hreflang`) | 🔜 |
|
|
260
420
|
| Automatic OG image generation | 🔜 |
|
|
261
421
|
| Predefined templates (`--template blog`) | 🔜 |
|
|
262
|
-
|
|
|
422
|
+
| JSON-LD structured data | ✅ |
|
|
423
|
+
| `<JsonLd />` React component | ✅ |
|
|
263
424
|
|
|
264
425
|
---
|
|
265
426
|
|
|
@@ -340,4 +501,4 @@ MIT © 2025 — Designed for modern Next.js developers 🧑💻
|
|
|
340
501
|
|
|
341
502
|
[⭐ Star us on GitHub](https://github.com/neysixx/seox) • [🐦 Follow on X](https://x.com/ks_nsx) • [📧 Contact](mailto:kylliansenrens3004@gmail.com)
|
|
342
503
|
|
|
343
|
-
</div>
|
|
504
|
+
</div>
|
package/dist/cli.cjs
CHANGED
|
@@ -1,57 +1,74 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';var commander=require('commander'),
|
|
2
|
+
'use strict';var commander=require('commander'),w=require('path'),t=require('chalk'),u=require('fs-extra'),P=require('ora'),A=require('prompts');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var w__default=/*#__PURE__*/_interopDefault(w);var t__default=/*#__PURE__*/_interopDefault(t);var u__default=/*#__PURE__*/_interopDefault(u);var P__default=/*#__PURE__*/_interopDefault(P);var A__default=/*#__PURE__*/_interopDefault(A);var m="SEOX";var c="seo.ts";var f=new commander.Command().name("seox").description(`\u{1F9E0} ${m} - Simplified SEO management for Next.js App Router`).version("1.0.0");var N={config:`import { Seox } from "seox/next";
|
|
3
3
|
|
|
4
4
|
export const seoConfig = new Seox({
|
|
5
5
|
name: "{{siteName}}",
|
|
6
6
|
url: "{{baseUrl}}",
|
|
7
7
|
title: {
|
|
8
8
|
default: "{{siteName}}",
|
|
9
|
-
template: "",
|
|
9
|
+
template: "%s | {{siteName}}",
|
|
10
10
|
},
|
|
11
11
|
description: "{{siteDescription}}",
|
|
12
12
|
keywords: [],
|
|
13
13
|
creator: "",
|
|
14
14
|
publisher: "",
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
authors: [
|
|
16
|
+
{
|
|
17
|
+
name: "",
|
|
18
|
+
url: "",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
manifest: "",
|
|
22
|
+
// JSON-LD structured data for rich snippets
|
|
23
|
+
jsonld: [
|
|
24
|
+
{
|
|
25
|
+
"@context": "https://schema.org",
|
|
26
|
+
"@type": "Organization",
|
|
27
|
+
name: "{{siteName}}",
|
|
28
|
+
url: "{{baseUrl}}",
|
|
29
|
+
// Add more fields: logo, address, contactPoint, sameAs...
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
})`};function g(n){return u__default.default.existsSync(`${process.cwd()}/src`)?w__default.default.join(process.cwd(),"src","lib",n):w__default.default.join(process.cwd(),"lib",n)}function S(n){let e={errors:[],warnings:[],suggestions:[]};return Object.entries(n.pages||{}).forEach(([s,o])=>{o.title?o.title.length>60&&e.warnings.push(`Page "${s}" : title too long (${o.title.length} > 60)`):e.errors.push(`Page "${s}" : title missing`),o.description?o.description.length>160&&e.warnings.push(`Page "${s}" : description too long (${o.description.length} > 160)`):e.errors.push(`Page "${s}" : description missing`),(!o.jsonld||o.jsonld.length===0)&&e.suggestions.push(`Page "${s}" : add JSON-LD to improve indexing`);}),e}async function O(){let n=[],e=u__default.default.existsSync(`${process.cwd()}/src`)?"src":"",s=w__default.default.join(process.cwd(),e,"app"),o=w__default.default.join(process.cwd(),e,"pages");return await u__default.default.pathExists(s)&&await $(s,n),await u__default.default.pathExists(o)&&await $(o,n),n}async function $(n,e,s){let o=await u__default.default.readdir(n,{withFileTypes:true});for(let i of o){let a=w__default.default.join(n,i.name);if(i.isDirectory())await $(a,e);else if(i.isFile()&&(i.name==="layout.tsx"||i.name==="page.tsx"||i.name==="layout.ts"||i.name==="page.ts")){let r=await u__default.default.readFile(a,"utf8"),l=r.includes("export const metadata"),d=l?v(r):void 0;e.push({path:a,hasMetadata:l,metadataContent:d});}}}function v(n){let e=n.split(`
|
|
33
|
+
`),s=[],o=false,i=0;for(let a of e){if(a.includes("export const metadata")){o=true,s.push(a),i=(a.match(/\{/g)||[]).length-(a.match(/\}/g)||[]).length;continue}if(o&&(s.push(a),i+=(a.match(/\{/g)||[]).length-(a.match(/\}/g)||[]).length,i===0))break}return s.join(`
|
|
34
|
+
`)}function M(n){return n.endsWith("layout.tsx")||n.endsWith("layout.ts")}function L(n){if(n.includes("<JsonLd"))return n;let e=n;if(e.includes("import { seoConfig } from '@/lib/seo'")&&!e.includes("import { JsonLd }")){let s=e.split(`
|
|
35
|
+
`),o=0;for(let i=0;i<s.length;i++)if(s[i].includes("import { seoConfig } from '@/lib/seo'")){o=i+1;break}s.splice(o,0,"import { JsonLd } from 'seox/next';"),e=s.join(`
|
|
36
|
+
`);}return e.includes("<head>")&&!e.includes("<JsonLd")?e=e.replace(/<head>(\s*)/,`<head>$1<JsonLd seo={seoConfig} />
|
|
37
|
+
$1`):e.includes("<head")&&!e.includes("<JsonLd")?e=e.replace(/(<head[^>]*>)(\s*)/,`$1$2<JsonLd seo={seoConfig} />
|
|
38
|
+
$2`):!e.includes("<head")&&e.includes("<html")&&(e=e.replace(/(<html[^>]*>)(\s*)/,`$1$2<head>
|
|
39
|
+
<JsonLd seo={seoConfig} />
|
|
40
|
+
</head>$2`)),e}async function x(n,e=false){let s=await u__default.default.readFile(n,"utf8");if(s.includes("export const metadata")&&!e)return false;let o=s;if(e&&s.includes("export const metadata")&&(o=s.replace(/export const metadata[^;]+;?\s*/gs,"")),!o.includes("import { seoConfig } from '@/lib/seo'")){let l=`import { seoConfig } from '@/lib/seo';
|
|
41
|
+
`,d=o.split(`
|
|
42
|
+
`),E=0;for(let y=0;y<d.length;y++)if(d[y].startsWith("import "))E=y+1;else if(d[y].trim()===""&&E>0)break;d.splice(E,0,l),o=d.join(`
|
|
43
|
+
`);}let i=`
|
|
28
44
|
export const metadata = seoConfig.configToMetadata();
|
|
29
|
-
`,
|
|
30
|
-
`),r=
|
|
31
|
-
`),await u__default.default.writeFile(
|
|
45
|
+
`,a=o.split(`
|
|
46
|
+
`),r=a.length;for(let l=0;l<a.length;l++)if(a[l].startsWith("export default")||a[l].startsWith("export function")||a[l].startsWith("export const")&&!a[l].includes("metadata")){r=l;break}return a.splice(r,0,i),o=a.join(`
|
|
47
|
+
`),M(n)&&(o=L(o)),await u__default.default.writeFile(n,o,"utf8"),true}f.command("init").description("Initialize the SEOX SEO configuration").action(async n=>{if(console.log(t__default.default.cyan.bold(`
|
|
32
48
|
\u{1F9E0} ${m} - Initialization
|
|
33
|
-
`)),await u__default.default.pathExists(g(c))){let{overwrite:o}=await
|
|
34
|
-
\u2705 File created : `)+
|
|
35
|
-
\u{1F4DD} Next step :`)),console.log(
|
|
49
|
+
`)),await u__default.default.pathExists(g(c))){let{overwrite:o}=await A__default.default({type:"confirm",name:"overwrite",message:`The ${g(c)} file already exists. Do you want to overwrite it?`,initial:false});if(!o){console.log(t__default.default.yellow("\u26A0\uFE0F Initialization canceled"));return}}let e=await A__default.default([{type:"text",name:"siteName",message:"Name of your site :",initial:"My Site"},{type:"text",name:"baseUrl",message:"Base URL (without trailing slash) :",initial:"https://mysite.com",validate:o=>o.startsWith("http")||"Invalid URL"},{type:"text",name:"siteDescription",message:"Default description :",initial:"A modern site built with Next.js"}]);if(!e.siteName){console.log(t__default.default.red("\u274C Initialization canceled"));return}let s=P__default.default("Creating files...").start();try{await u__default.default.ensureDir(w__default.default.join(process.cwd(),"lib"));let o=N.config;Object.entries(e).forEach(([i,a])=>{o=o.replace(new RegExp(`{{${i}}}`,"g"),a);}),await u__default.default.writeFile(g(c),o,"utf8"),s.succeed("Configuration created successfully!"),console.log(t__default.default.green(`
|
|
50
|
+
\u2705 File created : `)+t__default.default.gray(g(c))),console.log(t__default.default.cyan(`
|
|
51
|
+
\u{1F4DD} Next step :`)),console.log(t__default.default.white(" bunx seox configure")),console.log();}catch(o){s.fail("Error creating the configuration"),console.error(t__default.default.red(o.message)),process.exit(1);}});f.command("configure").description("Apply SEO configuration to all Next.js pages and layouts").option("--validate","Validate only without generating",false).option("--force","Force overwrite existing metadata without asking",false).action(async n=>{console.log(t__default.default.cyan.bold(`
|
|
36
52
|
\u2699\uFE0F ${m} - Configuration
|
|
37
|
-
`));let
|
|
38
|
-
\u{1F4A1} Run first: `)+
|
|
39
|
-
\u{1F4A1} Make sure you have an app/ or pages/ directory with layout.tsx/page.tsx files`));return}
|
|
40
|
-
\u{1F4C1} Files found:`)),
|
|
41
|
-
\u{1F504} Processing files without metadata...`));for(let r of
|
|
42
|
-
\u26A0\uFE0F ${
|
|
43
|
-
\u{1F504} Overwriting all existing metadata (--force)...`));for(let r of
|
|
44
|
-
\u{1F504} Processing files with existing metadata...`));for(let r of
|
|
45
|
-
\u274C Operation cancelled`));break}if(l.overwrite)try{await x(r.path,!0)&&(o.overwritten++,console.log(
|
|
53
|
+
`));let e=P__default.default("Reading configuration...").start();try{if(!await u__default.default.pathExists(g(c))){e.fail(`${c} file not found`),console.log(t__default.default.yellow(`
|
|
54
|
+
\u{1F4A1} Run first: `)+t__default.default.white("bunx seox init"));return}e.text="Scanning Next.js files...";let s=await O();if(s.length===0){e.fail("No Next.js layout or page files found"),console.log(t__default.default.yellow(`
|
|
55
|
+
\u{1F4A1} Make sure you have an app/ or pages/ directory with layout.tsx/page.tsx files`));return}e.succeed(`Found ${s.length} file(s) to process`),console.log(t__default.default.cyan(`
|
|
56
|
+
\u{1F4C1} Files found:`)),s.forEach(r=>{let l=r.hasMetadata?t__default.default.yellow("(has metadata)"):t__default.default.green("(no metadata)");console.log(t__default.default.gray(` \u2022 ${r.path} ${l}`));});let o={added:0,overwritten:0,skipped:0,errors:[]},i=s.filter(r=>r.hasMetadata),a=s.filter(r=>!r.hasMetadata);console.log(t__default.default.cyan(`
|
|
57
|
+
\u{1F504} Processing files without metadata...`));for(let r of a)try{await x(r.path,!1)&&(o.added++,console.log(t__default.default.green(` \u2713 Added metadata to ${r.path}`)));}catch(l){o.errors.push(`${r.path}: ${l.message}`),console.log(t__default.default.red(` \u2717 Error in ${r.path}`));}if(i.length>0)if(console.log(t__default.default.yellow(`
|
|
58
|
+
\u26A0\uFE0F ${i.length} file(s) already have metadata exports`)),n.force){console.log(t__default.default.cyan(`
|
|
59
|
+
\u{1F504} Overwriting all existing metadata (--force)...`));for(let r of i)try{await x(r.path,!0)&&(o.overwritten++,console.log(t__default.default.yellow(` \u2713 Overwritten ${r.path}`)));}catch(l){o.errors.push(`${r.path}: ${l.message}`),console.log(t__default.default.red(` \u2717 Error in ${r.path}`));}}else {console.log(t__default.default.cyan(`
|
|
60
|
+
\u{1F504} Processing files with existing metadata...`));for(let r of i){let l=await A__default.default({type:"confirm",name:"overwrite",message:`Overwrite metadata in ${r.path}?`,initial:!1});if(l.overwrite===void 0){console.log(t__default.default.yellow(`
|
|
61
|
+
\u274C Operation cancelled`));break}if(l.overwrite)try{await x(r.path,!0)&&(o.overwritten++,console.log(t__default.default.yellow(` \u2713 Overwritten ${r.path}`)));}catch(d){o.errors.push(`${r.path}: ${d.message}`),console.log(t__default.default.red(` \u2717 Error in ${r.path}`));}else o.skipped++,console.log(t__default.default.gray(` \u25CB Skipped ${r.path}`));}}console.log(t__default.default.green.bold(`
|
|
46
62
|
\u2705 Configuration Summary:
|
|
47
|
-
`)),console.log(
|
|
63
|
+
`)),console.log(t__default.default.gray(` \u2022 ${o.added} file(s) with metadata added`)),o.overwritten>0&&console.log(t__default.default.yellow(` \u2022 ${o.overwritten} file(s) overwritten`)),o.skipped>0&&console.log(t__default.default.gray(` \u2022 ${o.skipped} file(s) skipped`)),o.errors.length>0&&(console.log(t__default.default.red(` \u2022 ${o.errors.length} error(s):`)),o.errors.forEach(r=>{console.log(t__default.default.red(` - ${r}`));})),(o.added>0||o.overwritten>0)&&(console.log(t__default.default.cyan.bold(`
|
|
48
64
|
\u{1F4DD} Next Steps:
|
|
49
|
-
`)),console.log(
|
|
50
|
-
`)),console.log(
|
|
51
|
-
`)),console.log(
|
|
65
|
+
`)),console.log(t__default.default.white(" The following has been added to your files:")),console.log(t__default.default.gray(" import { seoConfig } from '@/lib/seo';")),console.log(t__default.default.gray(" import { JsonLd } from 'seox/next';")),console.log(t__default.default.gray(` export const metadata = seoConfig.configToMetadata();
|
|
66
|
+
`)),console.log(t__default.default.white(" For layout files, the <JsonLd /> component was also added:")),console.log(t__default.default.gray(" <head>")),console.log(t__default.default.gray(" <JsonLd seo={seoConfig} />")),console.log(t__default.default.gray(` </head>
|
|
67
|
+
`)),console.log(t__default.default.white(" To customize metadata for specific pages, you can:")),console.log(t__default.default.gray(" 1. Pass overrides to configToMetadata():")),console.log(t__default.default.gray(" export const metadata = seoConfig.configToMetadata({")),console.log(t__default.default.gray(' title: "Custom Page Title",')),console.log(t__default.default.gray(' description: "Custom description"')),console.log(t__default.default.gray(` });
|
|
68
|
+
`)),console.log(t__default.default.gray(" 2. Add JSON-LD schemas in your seo.ts config")),console.log());}catch(s){e.fail(`Error during configuration. You may have to check your configuration file in ${g(c)}.`),console.error(t__default.default.red(s.message)),process.exit(1);}});f.command("doctor").description("Check the validity of your SEO configuration").action(async()=>{console.log(t__default.default.cyan.bold(`
|
|
52
69
|
\u{1FA7A} ${m} - SEO Diagnostic
|
|
53
|
-
`));let
|
|
54
|
-
`));return}o.errors.length>0&&(console.log(
|
|
55
|
-
`)),o.errors.map(
|
|
56
|
-
`)),o.warnings.map(
|
|
57
|
-
`)),o.suggestions.map(
|
|
70
|
+
`));let n=P__default.default("Analyzing...").start();try{if(!await u__default.default.pathExists(g(c))){n.fail(`${c} file not found`);return}let s=(await import(g(c))).default;n.stop();let o=S(s);if(o.errors.length===0&&o.warnings.length===0){console.log(t__default.default.green(`\u2705 No issues detected ! Your SEO configuration is optimal.
|
|
71
|
+
`));return}o.errors.length>0&&(console.log(t__default.default.red.bold(`\u274C ERRORS :
|
|
72
|
+
`)),o.errors.map(i=>console.log(t__default.default.red(` \u2022 ${i}`))),console.log()),o.warnings.length>0&&(console.log(t__default.default.yellow.bold(`\u26A0\uFE0F WARNINGS :
|
|
73
|
+
`)),o.warnings.map(i=>console.log(t__default.default.yellow(` \u2022 ${i}`))),console.log()),o.suggestions.length>0&&(console.log(t__default.default.cyan.bold(`\u{1F4A1} SUGGESTIONS :
|
|
74
|
+
`)),o.suggestions.map(i=>console.log(t__default.default.cyan(` \u2022 ${i}`))),console.log());}catch(e){n.fail("Error during diagnosis"),console.error(t__default.default.red(e.message));}});f.parse(process.argv);var $o=f;module.exports=$o;
|
package/dist/cli.js
CHANGED
|
@@ -1,57 +1,74 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {Command}from'commander';import
|
|
2
|
+
import {Command}from'commander';import w from'path';import t from'chalk';import u from'fs-extra';import P from'ora';import A from'prompts';var m="SEOX";var c="seo.ts";var f=new Command().name("seox").description(`\u{1F9E0} ${m} - Simplified SEO management for Next.js App Router`).version("1.0.0");var N={config:`import { Seox } from "seox/next";
|
|
3
3
|
|
|
4
4
|
export const seoConfig = new Seox({
|
|
5
5
|
name: "{{siteName}}",
|
|
6
6
|
url: "{{baseUrl}}",
|
|
7
7
|
title: {
|
|
8
8
|
default: "{{siteName}}",
|
|
9
|
-
template: "",
|
|
9
|
+
template: "%s | {{siteName}}",
|
|
10
10
|
},
|
|
11
11
|
description: "{{siteDescription}}",
|
|
12
12
|
keywords: [],
|
|
13
13
|
creator: "",
|
|
14
14
|
publisher: "",
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
authors: [
|
|
16
|
+
{
|
|
17
|
+
name: "",
|
|
18
|
+
url: "",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
manifest: "",
|
|
22
|
+
// JSON-LD structured data for rich snippets
|
|
23
|
+
jsonld: [
|
|
24
|
+
{
|
|
25
|
+
"@context": "https://schema.org",
|
|
26
|
+
"@type": "Organization",
|
|
27
|
+
name: "{{siteName}}",
|
|
28
|
+
url: "{{baseUrl}}",
|
|
29
|
+
// Add more fields: logo, address, contactPoint, sameAs...
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
})`};function g(n){return u.existsSync(`${process.cwd()}/src`)?w.join(process.cwd(),"src","lib",n):w.join(process.cwd(),"lib",n)}function S(n){let e={errors:[],warnings:[],suggestions:[]};return Object.entries(n.pages||{}).forEach(([s,o])=>{o.title?o.title.length>60&&e.warnings.push(`Page "${s}" : title too long (${o.title.length} > 60)`):e.errors.push(`Page "${s}" : title missing`),o.description?o.description.length>160&&e.warnings.push(`Page "${s}" : description too long (${o.description.length} > 160)`):e.errors.push(`Page "${s}" : description missing`),(!o.jsonld||o.jsonld.length===0)&&e.suggestions.push(`Page "${s}" : add JSON-LD to improve indexing`);}),e}async function O(){let n=[],e=u.existsSync(`${process.cwd()}/src`)?"src":"",s=w.join(process.cwd(),e,"app"),o=w.join(process.cwd(),e,"pages");return await u.pathExists(s)&&await $(s,n),await u.pathExists(o)&&await $(o,n),n}async function $(n,e,s){let o=await u.readdir(n,{withFileTypes:true});for(let i of o){let a=w.join(n,i.name);if(i.isDirectory())await $(a,e);else if(i.isFile()&&(i.name==="layout.tsx"||i.name==="page.tsx"||i.name==="layout.ts"||i.name==="page.ts")){let r=await u.readFile(a,"utf8"),l=r.includes("export const metadata"),d=l?v(r):void 0;e.push({path:a,hasMetadata:l,metadataContent:d});}}}function v(n){let e=n.split(`
|
|
33
|
+
`),s=[],o=false,i=0;for(let a of e){if(a.includes("export const metadata")){o=true,s.push(a),i=(a.match(/\{/g)||[]).length-(a.match(/\}/g)||[]).length;continue}if(o&&(s.push(a),i+=(a.match(/\{/g)||[]).length-(a.match(/\}/g)||[]).length,i===0))break}return s.join(`
|
|
34
|
+
`)}function M(n){return n.endsWith("layout.tsx")||n.endsWith("layout.ts")}function L(n){if(n.includes("<JsonLd"))return n;let e=n;if(e.includes("import { seoConfig } from '@/lib/seo'")&&!e.includes("import { JsonLd }")){let s=e.split(`
|
|
35
|
+
`),o=0;for(let i=0;i<s.length;i++)if(s[i].includes("import { seoConfig } from '@/lib/seo'")){o=i+1;break}s.splice(o,0,"import { JsonLd } from 'seox/next';"),e=s.join(`
|
|
36
|
+
`);}return e.includes("<head>")&&!e.includes("<JsonLd")?e=e.replace(/<head>(\s*)/,`<head>$1<JsonLd seo={seoConfig} />
|
|
37
|
+
$1`):e.includes("<head")&&!e.includes("<JsonLd")?e=e.replace(/(<head[^>]*>)(\s*)/,`$1$2<JsonLd seo={seoConfig} />
|
|
38
|
+
$2`):!e.includes("<head")&&e.includes("<html")&&(e=e.replace(/(<html[^>]*>)(\s*)/,`$1$2<head>
|
|
39
|
+
<JsonLd seo={seoConfig} />
|
|
40
|
+
</head>$2`)),e}async function x(n,e=false){let s=await u.readFile(n,"utf8");if(s.includes("export const metadata")&&!e)return false;let o=s;if(e&&s.includes("export const metadata")&&(o=s.replace(/export const metadata[^;]+;?\s*/gs,"")),!o.includes("import { seoConfig } from '@/lib/seo'")){let l=`import { seoConfig } from '@/lib/seo';
|
|
41
|
+
`,d=o.split(`
|
|
42
|
+
`),E=0;for(let y=0;y<d.length;y++)if(d[y].startsWith("import "))E=y+1;else if(d[y].trim()===""&&E>0)break;d.splice(E,0,l),o=d.join(`
|
|
43
|
+
`);}let i=`
|
|
28
44
|
export const metadata = seoConfig.configToMetadata();
|
|
29
|
-
`,
|
|
30
|
-
`),r=
|
|
31
|
-
`),await u.writeFile(
|
|
45
|
+
`,a=o.split(`
|
|
46
|
+
`),r=a.length;for(let l=0;l<a.length;l++)if(a[l].startsWith("export default")||a[l].startsWith("export function")||a[l].startsWith("export const")&&!a[l].includes("metadata")){r=l;break}return a.splice(r,0,i),o=a.join(`
|
|
47
|
+
`),M(n)&&(o=L(o)),await u.writeFile(n,o,"utf8"),true}f.command("init").description("Initialize the SEOX SEO configuration").action(async n=>{if(console.log(t.cyan.bold(`
|
|
32
48
|
\u{1F9E0} ${m} - Initialization
|
|
33
|
-
`)),await u.pathExists(g(c))){let{overwrite:o}=await
|
|
34
|
-
\u2705 File created : `)+
|
|
35
|
-
\u{1F4DD} Next step :`)),console.log(
|
|
49
|
+
`)),await u.pathExists(g(c))){let{overwrite:o}=await A({type:"confirm",name:"overwrite",message:`The ${g(c)} file already exists. Do you want to overwrite it?`,initial:false});if(!o){console.log(t.yellow("\u26A0\uFE0F Initialization canceled"));return}}let e=await A([{type:"text",name:"siteName",message:"Name of your site :",initial:"My Site"},{type:"text",name:"baseUrl",message:"Base URL (without trailing slash) :",initial:"https://mysite.com",validate:o=>o.startsWith("http")||"Invalid URL"},{type:"text",name:"siteDescription",message:"Default description :",initial:"A modern site built with Next.js"}]);if(!e.siteName){console.log(t.red("\u274C Initialization canceled"));return}let s=P("Creating files...").start();try{await u.ensureDir(w.join(process.cwd(),"lib"));let o=N.config;Object.entries(e).forEach(([i,a])=>{o=o.replace(new RegExp(`{{${i}}}`,"g"),a);}),await u.writeFile(g(c),o,"utf8"),s.succeed("Configuration created successfully!"),console.log(t.green(`
|
|
50
|
+
\u2705 File created : `)+t.gray(g(c))),console.log(t.cyan(`
|
|
51
|
+
\u{1F4DD} Next step :`)),console.log(t.white(" bunx seox configure")),console.log();}catch(o){s.fail("Error creating the configuration"),console.error(t.red(o.message)),process.exit(1);}});f.command("configure").description("Apply SEO configuration to all Next.js pages and layouts").option("--validate","Validate only without generating",false).option("--force","Force overwrite existing metadata without asking",false).action(async n=>{console.log(t.cyan.bold(`
|
|
36
52
|
\u2699\uFE0F ${m} - Configuration
|
|
37
|
-
`));let
|
|
38
|
-
\u{1F4A1} Run first: `)+
|
|
39
|
-
\u{1F4A1} Make sure you have an app/ or pages/ directory with layout.tsx/page.tsx files`));return}
|
|
40
|
-
\u{1F4C1} Files found:`)),
|
|
41
|
-
\u{1F504} Processing files without metadata...`));for(let r of
|
|
42
|
-
\u26A0\uFE0F ${
|
|
43
|
-
\u{1F504} Overwriting all existing metadata (--force)...`));for(let r of
|
|
44
|
-
\u{1F504} Processing files with existing metadata...`));for(let r of
|
|
45
|
-
\u274C Operation cancelled`));break}if(l.overwrite)try{await x(r.path,!0)&&(o.overwritten++,console.log(
|
|
53
|
+
`));let e=P("Reading configuration...").start();try{if(!await u.pathExists(g(c))){e.fail(`${c} file not found`),console.log(t.yellow(`
|
|
54
|
+
\u{1F4A1} Run first: `)+t.white("bunx seox init"));return}e.text="Scanning Next.js files...";let s=await O();if(s.length===0){e.fail("No Next.js layout or page files found"),console.log(t.yellow(`
|
|
55
|
+
\u{1F4A1} Make sure you have an app/ or pages/ directory with layout.tsx/page.tsx files`));return}e.succeed(`Found ${s.length} file(s) to process`),console.log(t.cyan(`
|
|
56
|
+
\u{1F4C1} Files found:`)),s.forEach(r=>{let l=r.hasMetadata?t.yellow("(has metadata)"):t.green("(no metadata)");console.log(t.gray(` \u2022 ${r.path} ${l}`));});let o={added:0,overwritten:0,skipped:0,errors:[]},i=s.filter(r=>r.hasMetadata),a=s.filter(r=>!r.hasMetadata);console.log(t.cyan(`
|
|
57
|
+
\u{1F504} Processing files without metadata...`));for(let r of a)try{await x(r.path,!1)&&(o.added++,console.log(t.green(` \u2713 Added metadata to ${r.path}`)));}catch(l){o.errors.push(`${r.path}: ${l.message}`),console.log(t.red(` \u2717 Error in ${r.path}`));}if(i.length>0)if(console.log(t.yellow(`
|
|
58
|
+
\u26A0\uFE0F ${i.length} file(s) already have metadata exports`)),n.force){console.log(t.cyan(`
|
|
59
|
+
\u{1F504} Overwriting all existing metadata (--force)...`));for(let r of i)try{await x(r.path,!0)&&(o.overwritten++,console.log(t.yellow(` \u2713 Overwritten ${r.path}`)));}catch(l){o.errors.push(`${r.path}: ${l.message}`),console.log(t.red(` \u2717 Error in ${r.path}`));}}else {console.log(t.cyan(`
|
|
60
|
+
\u{1F504} Processing files with existing metadata...`));for(let r of i){let l=await A({type:"confirm",name:"overwrite",message:`Overwrite metadata in ${r.path}?`,initial:!1});if(l.overwrite===void 0){console.log(t.yellow(`
|
|
61
|
+
\u274C Operation cancelled`));break}if(l.overwrite)try{await x(r.path,!0)&&(o.overwritten++,console.log(t.yellow(` \u2713 Overwritten ${r.path}`)));}catch(d){o.errors.push(`${r.path}: ${d.message}`),console.log(t.red(` \u2717 Error in ${r.path}`));}else o.skipped++,console.log(t.gray(` \u25CB Skipped ${r.path}`));}}console.log(t.green.bold(`
|
|
46
62
|
\u2705 Configuration Summary:
|
|
47
|
-
`)),console.log(
|
|
63
|
+
`)),console.log(t.gray(` \u2022 ${o.added} file(s) with metadata added`)),o.overwritten>0&&console.log(t.yellow(` \u2022 ${o.overwritten} file(s) overwritten`)),o.skipped>0&&console.log(t.gray(` \u2022 ${o.skipped} file(s) skipped`)),o.errors.length>0&&(console.log(t.red(` \u2022 ${o.errors.length} error(s):`)),o.errors.forEach(r=>{console.log(t.red(` - ${r}`));})),(o.added>0||o.overwritten>0)&&(console.log(t.cyan.bold(`
|
|
48
64
|
\u{1F4DD} Next Steps:
|
|
49
|
-
`)),console.log(
|
|
50
|
-
`)),console.log(
|
|
51
|
-
`)),console.log(
|
|
65
|
+
`)),console.log(t.white(" The following has been added to your files:")),console.log(t.gray(" import { seoConfig } from '@/lib/seo';")),console.log(t.gray(" import { JsonLd } from 'seox/next';")),console.log(t.gray(` export const metadata = seoConfig.configToMetadata();
|
|
66
|
+
`)),console.log(t.white(" For layout files, the <JsonLd /> component was also added:")),console.log(t.gray(" <head>")),console.log(t.gray(" <JsonLd seo={seoConfig} />")),console.log(t.gray(` </head>
|
|
67
|
+
`)),console.log(t.white(" To customize metadata for specific pages, you can:")),console.log(t.gray(" 1. Pass overrides to configToMetadata():")),console.log(t.gray(" export const metadata = seoConfig.configToMetadata({")),console.log(t.gray(' title: "Custom Page Title",')),console.log(t.gray(' description: "Custom description"')),console.log(t.gray(` });
|
|
68
|
+
`)),console.log(t.gray(" 2. Add JSON-LD schemas in your seo.ts config")),console.log());}catch(s){e.fail(`Error during configuration. You may have to check your configuration file in ${g(c)}.`),console.error(t.red(s.message)),process.exit(1);}});f.command("doctor").description("Check the validity of your SEO configuration").action(async()=>{console.log(t.cyan.bold(`
|
|
52
69
|
\u{1FA7A} ${m} - SEO Diagnostic
|
|
53
|
-
`));let
|
|
54
|
-
`));return}o.errors.length>0&&(console.log(
|
|
55
|
-
`)),o.errors.map(
|
|
56
|
-
`)),o.warnings.map(
|
|
57
|
-
`)),o.suggestions.map(
|
|
70
|
+
`));let n=P("Analyzing...").start();try{if(!await u.pathExists(g(c))){n.fail(`${c} file not found`);return}let s=(await import(g(c))).default;n.stop();let o=S(s);if(o.errors.length===0&&o.warnings.length===0){console.log(t.green(`\u2705 No issues detected ! Your SEO configuration is optimal.
|
|
71
|
+
`));return}o.errors.length>0&&(console.log(t.red.bold(`\u274C ERRORS :
|
|
72
|
+
`)),o.errors.map(i=>console.log(t.red(` \u2022 ${i}`))),console.log()),o.warnings.length>0&&(console.log(t.yellow.bold(`\u26A0\uFE0F WARNINGS :
|
|
73
|
+
`)),o.warnings.map(i=>console.log(t.yellow(` \u2022 ${i}`))),console.log()),o.suggestions.length>0&&(console.log(t.cyan.bold(`\u{1F4A1} SUGGESTIONS :
|
|
74
|
+
`)),o.suggestions.map(i=>console.log(t.cyan(` \u2022 ${i}`))),console.log());}catch(e){n.fail("Error during diagnosis"),console.error(t.red(e.message));}});f.parse(process.argv);var $o=f;export{$o as default};
|
package/dist/next.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
'use strict';var n=class{config;constructor(t){this.config=t;}configToMetadata(t){let o={...this.config,...t},
|
|
1
|
+
'use strict';var i=require('react');function _interopNamespace(e){if(e&&e.__esModule)return e;var n=Object.create(null);if(e){Object.keys(e).forEach(function(k){if(k!=='default'){var d=Object.getOwnPropertyDescriptor(e,k);Object.defineProperty(n,k,d.get?d:{enumerable:true,get:function(){return e[k]}});}})}n.default=e;return Object.freeze(n)}var i__namespace=/*#__PURE__*/_interopNamespace(i);var s=class{config;constructor(t){this.config=t;}configToMetadata(t){let o={...this.config,...t},n={title:o.title,description:o.description,keywords:o.keywords,creator:o.creator,publisher:o.publisher,authors:o.authors?.map(e=>({name:e.name,url:e.url})),manifest:o.manifest,icons:o.icons?{...this.config.icons,...t?.icons}:void 0,formatDetection:o.formatDetection?{...this.config.formatDetection,...t?.formatDetection}:void 0,openGraph:o.openGraph?{...this.config.openGraph,...t?.openGraph,url:t?.openGraph?.url??this.config.openGraph?.url??this.config.url,siteName:t?.openGraph?.siteName??this.config.openGraph?.siteName??this.config.name}:void 0,twitter:o.twitter?{...this.config.twitter,...t?.twitter}:void 0,robots:o.robots?{...this.config.robots,...t?.robots,googleBot:t?.robots?.googleBot||this.config.robots?.googleBot?{...this.config.robots?.googleBot,...t?.robots?.googleBot}:void 0}:void 0};return Object.fromEntries(Object.entries(n).filter(([e,r])=>r!==void 0))}generatePageMetadata(t){return this.configToMetadata(t)}getJsonLd(t){let o=this.config.jsonld??[],n=t??[];return [...o,...n]}getJsonLdStrings(t){return this.getJsonLd(t).map(o=>JSON.stringify(o))}};function f({seo:a,additionalSchemas:t}){let o=a.getJsonLd(t);return !o||o.length===0?null:i__namespace.createElement(i__namespace.Fragment,null,o.map((n,e)=>i__namespace.createElement("script",{key:`jsonld-${e}`,type:"application/ld+json",dangerouslySetInnerHTML:{__html:JSON.stringify(n)}})))}exports.JsonLd=f;exports.Seox=s;
|
package/dist/next.d.cts
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
|
-
import { SEOConfig } from './index.cjs';
|
|
1
|
+
import { SEOConfig, SEOJsonLd } from './index.cjs';
|
|
2
2
|
import { Metadata } from 'next';
|
|
3
|
+
import * as React from 'react';
|
|
3
4
|
import 'next/dist/lib/metadata/types/metadata-types';
|
|
4
5
|
import 'next/dist/lib/metadata/types/opengraph-types';
|
|
5
6
|
|
|
6
7
|
declare class Seox {
|
|
7
|
-
|
|
8
|
+
config: SEOConfig;
|
|
8
9
|
constructor(config: SEOConfig);
|
|
9
10
|
configToMetadata(overrides?: Partial<SEOConfig>): Metadata;
|
|
10
11
|
generatePageMetadata(overrides?: Partial<SEOConfig>): Metadata;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the JSON-LD schemas configured for this SEO config
|
|
14
|
+
* @param overrides - Optional additional JSON-LD schemas to include
|
|
15
|
+
* @returns Array of JSON-LD schema objects
|
|
16
|
+
*/
|
|
17
|
+
getJsonLd(overrides?: SEOJsonLd[]): SEOJsonLd[];
|
|
18
|
+
/**
|
|
19
|
+
* Returns the JSON-LD schemas as stringified JSON strings ready for injection
|
|
20
|
+
* @param overrides - Optional additional JSON-LD schemas to include
|
|
21
|
+
* @returns Array of stringified JSON-LD schemas
|
|
22
|
+
*/
|
|
23
|
+
getJsonLdStrings(overrides?: SEOJsonLd[]): string[];
|
|
11
24
|
}
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
interface JsonLdProps {
|
|
27
|
+
seo: Seox;
|
|
28
|
+
additionalSchemas?: SEOJsonLd[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* React component to inject JSON-LD structured data into the page
|
|
32
|
+
* Use this component in your layout.tsx inside the <head> tag
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* import { seoConfig } from '@/lib/seo';
|
|
37
|
+
* import { JsonLd } from 'seox/next';
|
|
38
|
+
*
|
|
39
|
+
* export default function RootLayout({ children }) {
|
|
40
|
+
* return (
|
|
41
|
+
* <html lang="fr">
|
|
42
|
+
* <head>
|
|
43
|
+
* <JsonLd seo={seoConfig} />
|
|
44
|
+
* </head>
|
|
45
|
+
* <body>{children}</body>
|
|
46
|
+
* </html>
|
|
47
|
+
* );
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function JsonLd({ seo, additionalSchemas }: JsonLdProps): React.JSX.Element | null;
|
|
52
|
+
|
|
53
|
+
export { JsonLd, SEOConfig, SEOJsonLd, Seox };
|
package/dist/next.d.ts
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
|
-
import { SEOConfig } from './index.js';
|
|
1
|
+
import { SEOConfig, SEOJsonLd } from './index.js';
|
|
2
2
|
import { Metadata } from 'next';
|
|
3
|
+
import * as React from 'react';
|
|
3
4
|
import 'next/dist/lib/metadata/types/metadata-types';
|
|
4
5
|
import 'next/dist/lib/metadata/types/opengraph-types';
|
|
5
6
|
|
|
6
7
|
declare class Seox {
|
|
7
|
-
|
|
8
|
+
config: SEOConfig;
|
|
8
9
|
constructor(config: SEOConfig);
|
|
9
10
|
configToMetadata(overrides?: Partial<SEOConfig>): Metadata;
|
|
10
11
|
generatePageMetadata(overrides?: Partial<SEOConfig>): Metadata;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the JSON-LD schemas configured for this SEO config
|
|
14
|
+
* @param overrides - Optional additional JSON-LD schemas to include
|
|
15
|
+
* @returns Array of JSON-LD schema objects
|
|
16
|
+
*/
|
|
17
|
+
getJsonLd(overrides?: SEOJsonLd[]): SEOJsonLd[];
|
|
18
|
+
/**
|
|
19
|
+
* Returns the JSON-LD schemas as stringified JSON strings ready for injection
|
|
20
|
+
* @param overrides - Optional additional JSON-LD schemas to include
|
|
21
|
+
* @returns Array of stringified JSON-LD schemas
|
|
22
|
+
*/
|
|
23
|
+
getJsonLdStrings(overrides?: SEOJsonLd[]): string[];
|
|
11
24
|
}
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
interface JsonLdProps {
|
|
27
|
+
seo: Seox;
|
|
28
|
+
additionalSchemas?: SEOJsonLd[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* React component to inject JSON-LD structured data into the page
|
|
32
|
+
* Use this component in your layout.tsx inside the <head> tag
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* import { seoConfig } from '@/lib/seo';
|
|
37
|
+
* import { JsonLd } from 'seox/next';
|
|
38
|
+
*
|
|
39
|
+
* export default function RootLayout({ children }) {
|
|
40
|
+
* return (
|
|
41
|
+
* <html lang="fr">
|
|
42
|
+
* <head>
|
|
43
|
+
* <JsonLd seo={seoConfig} />
|
|
44
|
+
* </head>
|
|
45
|
+
* <body>{children}</body>
|
|
46
|
+
* </html>
|
|
47
|
+
* );
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function JsonLd({ seo, additionalSchemas }: JsonLdProps): React.JSX.Element | null;
|
|
52
|
+
|
|
53
|
+
export { JsonLd, SEOConfig, SEOJsonLd, Seox };
|
package/dist/next.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
import*as i from'react';var s=class{config;constructor(t){this.config=t;}configToMetadata(t){let o={...this.config,...t},n={title:o.title,description:o.description,keywords:o.keywords,creator:o.creator,publisher:o.publisher,authors:o.authors?.map(e=>({name:e.name,url:e.url})),manifest:o.manifest,icons:o.icons?{...this.config.icons,...t?.icons}:void 0,formatDetection:o.formatDetection?{...this.config.formatDetection,...t?.formatDetection}:void 0,openGraph:o.openGraph?{...this.config.openGraph,...t?.openGraph,url:t?.openGraph?.url??this.config.openGraph?.url??this.config.url,siteName:t?.openGraph?.siteName??this.config.openGraph?.siteName??this.config.name}:void 0,twitter:o.twitter?{...this.config.twitter,...t?.twitter}:void 0,robots:o.robots?{...this.config.robots,...t?.robots,googleBot:t?.robots?.googleBot||this.config.robots?.googleBot?{...this.config.robots?.googleBot,...t?.robots?.googleBot}:void 0}:void 0};return Object.fromEntries(Object.entries(n).filter(([e,r])=>r!==void 0))}generatePageMetadata(t){return this.configToMetadata(t)}getJsonLd(t){let o=this.config.jsonld??[],n=t??[];return [...o,...n]}getJsonLdStrings(t){return this.getJsonLd(t).map(o=>JSON.stringify(o))}};function f({seo:a,additionalSchemas:t}){let o=a.getJsonLd(t);return !o||o.length===0?null:i.createElement(i.Fragment,null,o.map((n,e)=>i.createElement("script",{key:`jsonld-${e}`,type:"application/ld+json",dangerouslySetInnerHTML:{__html:JSON.stringify(n)}})))}export{f as JsonLd,s as Seox};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seox",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"test": "bun test",
|
|
44
44
|
"typecheck": "tsc --noEmit",
|
|
45
45
|
"prepublishOnly": "bun run build && bun run test",
|
|
46
|
-
"
|
|
46
|
+
"ci": "bun run typecheck && bun run format:check && bun run test && bun run build"
|
|
47
47
|
},
|
|
48
48
|
"keywords": [
|
|
49
49
|
"nextjs",
|