seox 1.1.1 → 1.3.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/dist/cli.cjs +37 -37
- package/dist/cli.js +37 -37
- package/dist/index.d.cts +15 -75
- package/dist/index.d.ts +15 -75
- package/dist/next.cjs +1 -1
- package/dist/next.d.cts +0 -2
- package/dist/next.d.ts +0 -2
- package/dist/next.js +1 -1
- package/package.json +4 -1
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';var commander=require('commander'),w=require('path'),
|
|
2
|
+
'use strict';var commander=require('commander'),w=require('path'),n=require('chalk'),u=require('fs-extra'),j=require('ora'),A=require('prompts');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var w__default=/*#__PURE__*/_interopDefault(w);var n__default=/*#__PURE__*/_interopDefault(n);var u__default=/*#__PURE__*/_interopDefault(u);var j__default=/*#__PURE__*/_interopDefault(j);var A__default=/*#__PURE__*/_interopDefault(A);var m="SEOX";var c="seo.ts";var p=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}}",
|
|
@@ -29,46 +29,46 @@ export const seoConfig = new Seox({
|
|
|
29
29
|
// Add more fields: logo, address, contactPoint, sameAs...
|
|
30
30
|
},
|
|
31
31
|
],
|
|
32
|
-
})`};function g(
|
|
33
|
-
`),s=[],
|
|
34
|
-
`)}function
|
|
35
|
-
`),
|
|
36
|
-
`);}return
|
|
37
|
-
$1`):
|
|
38
|
-
$2`):!
|
|
32
|
+
})`};function g(t){return u__default.default.existsSync(`${process.cwd()}/src`)?w__default.default.join(process.cwd(),"src","lib",t):w__default.default.join(process.cwd(),"lib",t)}function M(t){return typeof t=="string"?t:t&&typeof t=="object"&&"default"in t?t.default:null}function b(t){let o={errors:[],warnings:[],suggestions:[]},s=M(t.title);return s?s.length>60&&o.warnings.push(`title is too long (${s.length} > 60 characters)`):o.errors.push("title is missing"),t.description?t.description.length>160&&o.warnings.push(`description is too long (${t.description.length} > 160 characters)`):o.errors.push("description is missing"),(!t.jsonld||t.jsonld.length===0)&&o.suggestions.push("add JSON-LD structured data to improve search engine indexing"),o}async function O(){let t=[],o=u__default.default.existsSync(`${process.cwd()}/src`)?"src":"",s=w__default.default.join(process.cwd(),o,"app"),e=w__default.default.join(process.cwd(),o,"pages");return await u__default.default.pathExists(s)&&await $(s,t),await u__default.default.pathExists(e)&&await $(e,t),t}async function $(t,o,s){let e=await u__default.default.readdir(t,{withFileTypes:true});for(let r of e){let a=w__default.default.join(t,r.name);if(r.isDirectory())await $(a,o);else if(r.isFile()&&(r.name==="layout.tsx"||r.name==="page.tsx"||r.name==="layout.ts"||r.name==="page.ts")){let i=await u__default.default.readFile(a,"utf8"),l=i.includes("export const metadata"),f=l?v(i):void 0;o.push({path:a,hasMetadata:l,metadataContent:f});}}}function v(t){let o=t.split(`
|
|
33
|
+
`),s=[],e=false,r=0;for(let a of o){if(a.includes("export const metadata")){e=true,s.push(a),r=(a.match(/\{/g)||[]).length-(a.match(/\}/g)||[]).length;continue}if(e&&(s.push(a),r+=(a.match(/\{/g)||[]).length-(a.match(/\}/g)||[]).length,r===0))break}return s.join(`
|
|
34
|
+
`)}function I(t){return t.endsWith("layout.tsx")||t.endsWith("layout.ts")}function L(t){if(t.includes("<JsonLd"))return t;let o=t;if(o.includes("import { seoConfig } from '@/lib/seo'")&&!o.includes("import { JsonLd }")){let s=o.split(`
|
|
35
|
+
`),e=0;for(let r=0;r<s.length;r++)if(s[r].includes("import { seoConfig } from '@/lib/seo'")){e=r+1;break}s.splice(e,0,"import { JsonLd } from 'seox/next';"),o=s.join(`
|
|
36
|
+
`);}return o.includes("<head>")&&!o.includes("<JsonLd")?o=o.replace(/<head>(\s*)/,`<head>$1<JsonLd seo={seoConfig} />
|
|
37
|
+
$1`):o.includes("<head")&&!o.includes("<JsonLd")?o=o.replace(/(<head[^>]*>)(\s*)/,`$1$2<JsonLd seo={seoConfig} />
|
|
38
|
+
$2`):!o.includes("<head")&&o.includes("<html")&&(o=o.replace(/(<html[^>]*>)(\s*)/,`$1$2<head>
|
|
39
39
|
<JsonLd seo={seoConfig} />
|
|
40
|
-
</head>$2`)),
|
|
41
|
-
`,
|
|
42
|
-
`),E=0;for(let y=0;y<
|
|
43
|
-
`);}let
|
|
40
|
+
</head>$2`)),o}async function x(t,o=false){let s=await u__default.default.readFile(t,"utf8");if(s.includes("export const metadata")&&!o)return false;let e=s;if(o&&s.includes("export const metadata")&&(e=s.replace(/export const metadata[^;]+;?\s*/gs,"")),!e.includes("import { seoConfig } from '@/lib/seo'")){let l=`import { seoConfig } from '@/lib/seo';
|
|
41
|
+
`,f=e.split(`
|
|
42
|
+
`),E=0;for(let y=0;y<f.length;y++)if(f[y].startsWith("import "))E=y+1;else if(f[y].trim()===""&&E>0)break;f.splice(E,0,l),e=f.join(`
|
|
43
|
+
`);}let r=`
|
|
44
44
|
export const metadata = seoConfig.configToMetadata();
|
|
45
|
-
`,a=
|
|
46
|
-
`),
|
|
47
|
-
`),
|
|
45
|
+
`,a=e.split(`
|
|
46
|
+
`),i=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")){i=l;break}return a.splice(i,0,r),e=a.join(`
|
|
47
|
+
`),I(t)&&(e=L(e)),await u__default.default.writeFile(t,e,"utf8"),true}p.command("init").description("Initialize the SEOX SEO configuration").action(async t=>{if(console.log(n__default.default.cyan.bold(`
|
|
48
48
|
\u{1F9E0} ${m} - Initialization
|
|
49
|
-
`)),await u__default.default.pathExists(g(c))){let{overwrite:
|
|
50
|
-
\u2705 File created : `)+
|
|
51
|
-
\u{1F4DD} Next step :`)),console.log(
|
|
49
|
+
`)),await u__default.default.pathExists(g(c))){let{overwrite:e}=await A__default.default({type:"confirm",name:"overwrite",message:`The ${g(c)} file already exists. Do you want to overwrite it?`,initial:false});if(!e){console.log(n__default.default.yellow("\u26A0\uFE0F Initialization canceled"));return}}let o=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:e=>e.startsWith("http")||"Invalid URL"},{type:"text",name:"siteDescription",message:"Default description :",initial:"A modern site built with Next.js"}]);if(!o.siteName){console.log(n__default.default.red("\u274C Initialization canceled"));return}let s=j__default.default("Creating files...").start();try{await u__default.default.ensureDir(w__default.default.join(process.cwd(),"lib"));let e=N.config;Object.entries(o).forEach(([r,a])=>{e=e.replace(new RegExp(`{{${r}}}`,"g"),a);}),await u__default.default.writeFile(g(c),e,"utf8"),s.succeed("Configuration created successfully!"),console.log(n__default.default.green(`
|
|
50
|
+
\u2705 File created : `)+n__default.default.gray(g(c))),console.log(n__default.default.cyan(`
|
|
51
|
+
\u{1F4DD} Next step :`)),console.log(n__default.default.white(" bunx seox configure")),console.log();}catch(e){s.fail("Error creating the configuration"),console.error(n__default.default.red(e.message)),process.exit(1);}});p.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 t=>{console.log(n__default.default.cyan.bold(`
|
|
52
52
|
\u2699\uFE0F ${m} - Configuration
|
|
53
|
-
`));let
|
|
54
|
-
\u{1F4A1} Run first: `)+
|
|
55
|
-
\u{1F4A1} Make sure you have an app/ or pages/ directory with layout.tsx/page.tsx files`));return}
|
|
56
|
-
\u{1F4C1} Files found:`)),s.forEach(
|
|
57
|
-
\u{1F504} Processing files without metadata...`));for(let
|
|
58
|
-
\u26A0\uFE0F ${
|
|
59
|
-
\u{1F504} Overwriting all existing metadata (--force)...`));for(let
|
|
60
|
-
\u{1F504} Processing files with existing metadata...`));for(let
|
|
61
|
-
\u274C Operation cancelled`));break}if(l.overwrite)try{await x(
|
|
53
|
+
`));let o=j__default.default("Reading configuration...").start();try{if(!await u__default.default.pathExists(g(c))){o.fail(`${c} file not found`),console.log(n__default.default.yellow(`
|
|
54
|
+
\u{1F4A1} Run first: `)+n__default.default.white("bunx seox init"));return}o.text="Scanning Next.js files...";let s=await O();if(s.length===0){o.fail("No Next.js layout or page files found"),console.log(n__default.default.yellow(`
|
|
55
|
+
\u{1F4A1} Make sure you have an app/ or pages/ directory with layout.tsx/page.tsx files`));return}o.succeed(`Found ${s.length} file(s) to process`),console.log(n__default.default.cyan(`
|
|
56
|
+
\u{1F4C1} Files found:`)),s.forEach(i=>{let l=i.hasMetadata?n__default.default.yellow("(has metadata)"):n__default.default.green("(no metadata)");console.log(n__default.default.gray(` \u2022 ${i.path} ${l}`));});let e={added:0,overwritten:0,skipped:0,errors:[]},r=s.filter(i=>i.hasMetadata),a=s.filter(i=>!i.hasMetadata);console.log(n__default.default.cyan(`
|
|
57
|
+
\u{1F504} Processing files without metadata...`));for(let i of a)try{await x(i.path,!1)&&(e.added++,console.log(n__default.default.green(` \u2713 Added metadata to ${i.path}`)));}catch(l){e.errors.push(`${i.path}: ${l.message}`),console.log(n__default.default.red(` \u2717 Error in ${i.path}`));}if(r.length>0)if(console.log(n__default.default.yellow(`
|
|
58
|
+
\u26A0\uFE0F ${r.length} file(s) already have metadata exports`)),t.force){console.log(n__default.default.cyan(`
|
|
59
|
+
\u{1F504} Overwriting all existing metadata (--force)...`));for(let i of r)try{await x(i.path,!0)&&(e.overwritten++,console.log(n__default.default.yellow(` \u2713 Overwritten ${i.path}`)));}catch(l){e.errors.push(`${i.path}: ${l.message}`),console.log(n__default.default.red(` \u2717 Error in ${i.path}`));}}else {console.log(n__default.default.cyan(`
|
|
60
|
+
\u{1F504} Processing files with existing metadata...`));for(let i of r){let l=await A__default.default({type:"confirm",name:"overwrite",message:`Overwrite metadata in ${i.path}?`,initial:!1});if(l.overwrite===void 0){console.log(n__default.default.yellow(`
|
|
61
|
+
\u274C Operation cancelled`));break}if(l.overwrite)try{await x(i.path,!0)&&(e.overwritten++,console.log(n__default.default.yellow(` \u2713 Overwritten ${i.path}`)));}catch(f){e.errors.push(`${i.path}: ${f.message}`),console.log(n__default.default.red(` \u2717 Error in ${i.path}`));}else e.skipped++,console.log(n__default.default.gray(` \u25CB Skipped ${i.path}`));}}console.log(n__default.default.green.bold(`
|
|
62
62
|
\u2705 Configuration Summary:
|
|
63
|
-
`)),console.log(
|
|
63
|
+
`)),console.log(n__default.default.gray(` \u2022 ${e.added} file(s) with metadata added`)),e.overwritten>0&&console.log(n__default.default.yellow(` \u2022 ${e.overwritten} file(s) overwritten`)),e.skipped>0&&console.log(n__default.default.gray(` \u2022 ${e.skipped} file(s) skipped`)),e.errors.length>0&&(console.log(n__default.default.red(` \u2022 ${e.errors.length} error(s):`)),e.errors.forEach(i=>{console.log(n__default.default.red(` - ${i}`));})),(e.added>0||e.overwritten>0)&&(console.log(n__default.default.cyan.bold(`
|
|
64
64
|
\u{1F4DD} Next Steps:
|
|
65
|
-
`)),console.log(
|
|
66
|
-
`)),console.log(
|
|
67
|
-
`)),console.log(
|
|
68
|
-
`)),console.log(
|
|
65
|
+
`)),console.log(n__default.default.white(" The following has been added to your files:")),console.log(n__default.default.gray(" import { seoConfig } from '@/lib/seo';")),console.log(n__default.default.gray(" import { JsonLd } from 'seox/next';")),console.log(n__default.default.gray(` export const metadata = seoConfig.configToMetadata();
|
|
66
|
+
`)),console.log(n__default.default.white(" For layout files, the <JsonLd /> component was also added:")),console.log(n__default.default.gray(" <head>")),console.log(n__default.default.gray(" <JsonLd seo={seoConfig} />")),console.log(n__default.default.gray(` </head>
|
|
67
|
+
`)),console.log(n__default.default.white(" To customize metadata for specific pages, you can:")),console.log(n__default.default.gray(" 1. Pass overrides to configToMetadata():")),console.log(n__default.default.gray(" export const metadata = seoConfig.configToMetadata({")),console.log(n__default.default.gray(' title: "Custom Page Title",')),console.log(n__default.default.gray(' description: "Custom description"')),console.log(n__default.default.gray(` });
|
|
68
|
+
`)),console.log(n__default.default.gray(" 2. Add JSON-LD schemas in your seo.ts config")),console.log());}catch(s){o.fail(`Error during configuration. You may have to check your configuration file in ${g(c)}.`),console.error(n__default.default.red(s.message)),process.exit(1);}});p.command("doctor").description("Check the validity of your SEO configuration").action(async()=>{console.log(n__default.default.cyan.bold(`
|
|
69
69
|
\u{1FA7A} ${m} - SEO Diagnostic
|
|
70
|
-
`));let
|
|
71
|
-
`));return}
|
|
72
|
-
`)),
|
|
73
|
-
`)),
|
|
74
|
-
`)),
|
|
70
|
+
`));let t=j__default.default("Analyzing...").start();try{if(!await u__default.default.pathExists(g(c))){t.fail(`${c} file not found in ${g(c)}`);return}let s=(await import(g(c))).seoConfig;if(!s||!s.config){t.fail('seoConfig not found in configuration file. Make sure you export "seoConfig".');return}t.stop();let e=b(s.config);if(e.errors.length===0&&e.warnings.length===0){console.log(n__default.default.green(`\u2705 No issues detected ! Your SEO configuration is optimal.
|
|
71
|
+
`));return}e.errors.length>0&&(console.log(n__default.default.red.bold(`\u274C ERRORS :
|
|
72
|
+
`)),e.errors.map(r=>console.log(n__default.default.red(` \u2022 ${r}`))),console.log()),e.warnings.length>0&&(console.log(n__default.default.yellow.bold(`\u26A0\uFE0F WARNINGS :
|
|
73
|
+
`)),e.warnings.map(r=>console.log(n__default.default.yellow(` \u2022 ${r}`))),console.log()),e.suggestions.length>0&&(console.log(n__default.default.cyan.bold(`\u{1F4A1} SUGGESTIONS :
|
|
74
|
+
`)),e.suggestions.map(r=>console.log(n__default.default.cyan(` \u2022 ${r}`))),console.log());}catch(o){t.fail("Error during diagnosis"),console.error(n__default.default.red(o.message));}});p.parse(process.argv);var $e=p;module.exports=$e;
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {Command}from'commander';import w from'path';import
|
|
2
|
+
import {Command}from'commander';import w from'path';import n from'chalk';import u from'fs-extra';import j from'ora';import A from'prompts';var m="SEOX";var c="seo.ts";var p=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}}",
|
|
@@ -29,46 +29,46 @@ export const seoConfig = new Seox({
|
|
|
29
29
|
// Add more fields: logo, address, contactPoint, sameAs...
|
|
30
30
|
},
|
|
31
31
|
],
|
|
32
|
-
})`};function g(
|
|
33
|
-
`),s=[],
|
|
34
|
-
`)}function
|
|
35
|
-
`),
|
|
36
|
-
`);}return
|
|
37
|
-
$1`):
|
|
38
|
-
$2`):!
|
|
32
|
+
})`};function g(t){return u.existsSync(`${process.cwd()}/src`)?w.join(process.cwd(),"src","lib",t):w.join(process.cwd(),"lib",t)}function M(t){return typeof t=="string"?t:t&&typeof t=="object"&&"default"in t?t.default:null}function b(t){let o={errors:[],warnings:[],suggestions:[]},s=M(t.title);return s?s.length>60&&o.warnings.push(`title is too long (${s.length} > 60 characters)`):o.errors.push("title is missing"),t.description?t.description.length>160&&o.warnings.push(`description is too long (${t.description.length} > 160 characters)`):o.errors.push("description is missing"),(!t.jsonld||t.jsonld.length===0)&&o.suggestions.push("add JSON-LD structured data to improve search engine indexing"),o}async function O(){let t=[],o=u.existsSync(`${process.cwd()}/src`)?"src":"",s=w.join(process.cwd(),o,"app"),e=w.join(process.cwd(),o,"pages");return await u.pathExists(s)&&await $(s,t),await u.pathExists(e)&&await $(e,t),t}async function $(t,o,s){let e=await u.readdir(t,{withFileTypes:true});for(let r of e){let a=w.join(t,r.name);if(r.isDirectory())await $(a,o);else if(r.isFile()&&(r.name==="layout.tsx"||r.name==="page.tsx"||r.name==="layout.ts"||r.name==="page.ts")){let i=await u.readFile(a,"utf8"),l=i.includes("export const metadata"),f=l?v(i):void 0;o.push({path:a,hasMetadata:l,metadataContent:f});}}}function v(t){let o=t.split(`
|
|
33
|
+
`),s=[],e=false,r=0;for(let a of o){if(a.includes("export const metadata")){e=true,s.push(a),r=(a.match(/\{/g)||[]).length-(a.match(/\}/g)||[]).length;continue}if(e&&(s.push(a),r+=(a.match(/\{/g)||[]).length-(a.match(/\}/g)||[]).length,r===0))break}return s.join(`
|
|
34
|
+
`)}function I(t){return t.endsWith("layout.tsx")||t.endsWith("layout.ts")}function L(t){if(t.includes("<JsonLd"))return t;let o=t;if(o.includes("import { seoConfig } from '@/lib/seo'")&&!o.includes("import { JsonLd }")){let s=o.split(`
|
|
35
|
+
`),e=0;for(let r=0;r<s.length;r++)if(s[r].includes("import { seoConfig } from '@/lib/seo'")){e=r+1;break}s.splice(e,0,"import { JsonLd } from 'seox/next';"),o=s.join(`
|
|
36
|
+
`);}return o.includes("<head>")&&!o.includes("<JsonLd")?o=o.replace(/<head>(\s*)/,`<head>$1<JsonLd seo={seoConfig} />
|
|
37
|
+
$1`):o.includes("<head")&&!o.includes("<JsonLd")?o=o.replace(/(<head[^>]*>)(\s*)/,`$1$2<JsonLd seo={seoConfig} />
|
|
38
|
+
$2`):!o.includes("<head")&&o.includes("<html")&&(o=o.replace(/(<html[^>]*>)(\s*)/,`$1$2<head>
|
|
39
39
|
<JsonLd seo={seoConfig} />
|
|
40
|
-
</head>$2`)),
|
|
41
|
-
`,
|
|
42
|
-
`),E=0;for(let y=0;y<
|
|
43
|
-
`);}let
|
|
40
|
+
</head>$2`)),o}async function x(t,o=false){let s=await u.readFile(t,"utf8");if(s.includes("export const metadata")&&!o)return false;let e=s;if(o&&s.includes("export const metadata")&&(e=s.replace(/export const metadata[^;]+;?\s*/gs,"")),!e.includes("import { seoConfig } from '@/lib/seo'")){let l=`import { seoConfig } from '@/lib/seo';
|
|
41
|
+
`,f=e.split(`
|
|
42
|
+
`),E=0;for(let y=0;y<f.length;y++)if(f[y].startsWith("import "))E=y+1;else if(f[y].trim()===""&&E>0)break;f.splice(E,0,l),e=f.join(`
|
|
43
|
+
`);}let r=`
|
|
44
44
|
export const metadata = seoConfig.configToMetadata();
|
|
45
|
-
`,a=
|
|
46
|
-
`),
|
|
47
|
-
`),
|
|
45
|
+
`,a=e.split(`
|
|
46
|
+
`),i=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")){i=l;break}return a.splice(i,0,r),e=a.join(`
|
|
47
|
+
`),I(t)&&(e=L(e)),await u.writeFile(t,e,"utf8"),true}p.command("init").description("Initialize the SEOX SEO configuration").action(async t=>{if(console.log(n.cyan.bold(`
|
|
48
48
|
\u{1F9E0} ${m} - Initialization
|
|
49
|
-
`)),await u.pathExists(g(c))){let{overwrite:
|
|
50
|
-
\u2705 File created : `)+
|
|
51
|
-
\u{1F4DD} Next step :`)),console.log(
|
|
49
|
+
`)),await u.pathExists(g(c))){let{overwrite:e}=await A({type:"confirm",name:"overwrite",message:`The ${g(c)} file already exists. Do you want to overwrite it?`,initial:false});if(!e){console.log(n.yellow("\u26A0\uFE0F Initialization canceled"));return}}let o=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:e=>e.startsWith("http")||"Invalid URL"},{type:"text",name:"siteDescription",message:"Default description :",initial:"A modern site built with Next.js"}]);if(!o.siteName){console.log(n.red("\u274C Initialization canceled"));return}let s=j("Creating files...").start();try{await u.ensureDir(w.join(process.cwd(),"lib"));let e=N.config;Object.entries(o).forEach(([r,a])=>{e=e.replace(new RegExp(`{{${r}}}`,"g"),a);}),await u.writeFile(g(c),e,"utf8"),s.succeed("Configuration created successfully!"),console.log(n.green(`
|
|
50
|
+
\u2705 File created : `)+n.gray(g(c))),console.log(n.cyan(`
|
|
51
|
+
\u{1F4DD} Next step :`)),console.log(n.white(" bunx seox configure")),console.log();}catch(e){s.fail("Error creating the configuration"),console.error(n.red(e.message)),process.exit(1);}});p.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 t=>{console.log(n.cyan.bold(`
|
|
52
52
|
\u2699\uFE0F ${m} - Configuration
|
|
53
|
-
`));let
|
|
54
|
-
\u{1F4A1} Run first: `)+
|
|
55
|
-
\u{1F4A1} Make sure you have an app/ or pages/ directory with layout.tsx/page.tsx files`));return}
|
|
56
|
-
\u{1F4C1} Files found:`)),s.forEach(
|
|
57
|
-
\u{1F504} Processing files without metadata...`));for(let
|
|
58
|
-
\u26A0\uFE0F ${
|
|
59
|
-
\u{1F504} Overwriting all existing metadata (--force)...`));for(let
|
|
60
|
-
\u{1F504} Processing files with existing metadata...`));for(let
|
|
61
|
-
\u274C Operation cancelled`));break}if(l.overwrite)try{await x(
|
|
53
|
+
`));let o=j("Reading configuration...").start();try{if(!await u.pathExists(g(c))){o.fail(`${c} file not found`),console.log(n.yellow(`
|
|
54
|
+
\u{1F4A1} Run first: `)+n.white("bunx seox init"));return}o.text="Scanning Next.js files...";let s=await O();if(s.length===0){o.fail("No Next.js layout or page files found"),console.log(n.yellow(`
|
|
55
|
+
\u{1F4A1} Make sure you have an app/ or pages/ directory with layout.tsx/page.tsx files`));return}o.succeed(`Found ${s.length} file(s) to process`),console.log(n.cyan(`
|
|
56
|
+
\u{1F4C1} Files found:`)),s.forEach(i=>{let l=i.hasMetadata?n.yellow("(has metadata)"):n.green("(no metadata)");console.log(n.gray(` \u2022 ${i.path} ${l}`));});let e={added:0,overwritten:0,skipped:0,errors:[]},r=s.filter(i=>i.hasMetadata),a=s.filter(i=>!i.hasMetadata);console.log(n.cyan(`
|
|
57
|
+
\u{1F504} Processing files without metadata...`));for(let i of a)try{await x(i.path,!1)&&(e.added++,console.log(n.green(` \u2713 Added metadata to ${i.path}`)));}catch(l){e.errors.push(`${i.path}: ${l.message}`),console.log(n.red(` \u2717 Error in ${i.path}`));}if(r.length>0)if(console.log(n.yellow(`
|
|
58
|
+
\u26A0\uFE0F ${r.length} file(s) already have metadata exports`)),t.force){console.log(n.cyan(`
|
|
59
|
+
\u{1F504} Overwriting all existing metadata (--force)...`));for(let i of r)try{await x(i.path,!0)&&(e.overwritten++,console.log(n.yellow(` \u2713 Overwritten ${i.path}`)));}catch(l){e.errors.push(`${i.path}: ${l.message}`),console.log(n.red(` \u2717 Error in ${i.path}`));}}else {console.log(n.cyan(`
|
|
60
|
+
\u{1F504} Processing files with existing metadata...`));for(let i of r){let l=await A({type:"confirm",name:"overwrite",message:`Overwrite metadata in ${i.path}?`,initial:!1});if(l.overwrite===void 0){console.log(n.yellow(`
|
|
61
|
+
\u274C Operation cancelled`));break}if(l.overwrite)try{await x(i.path,!0)&&(e.overwritten++,console.log(n.yellow(` \u2713 Overwritten ${i.path}`)));}catch(f){e.errors.push(`${i.path}: ${f.message}`),console.log(n.red(` \u2717 Error in ${i.path}`));}else e.skipped++,console.log(n.gray(` \u25CB Skipped ${i.path}`));}}console.log(n.green.bold(`
|
|
62
62
|
\u2705 Configuration Summary:
|
|
63
|
-
`)),console.log(
|
|
63
|
+
`)),console.log(n.gray(` \u2022 ${e.added} file(s) with metadata added`)),e.overwritten>0&&console.log(n.yellow(` \u2022 ${e.overwritten} file(s) overwritten`)),e.skipped>0&&console.log(n.gray(` \u2022 ${e.skipped} file(s) skipped`)),e.errors.length>0&&(console.log(n.red(` \u2022 ${e.errors.length} error(s):`)),e.errors.forEach(i=>{console.log(n.red(` - ${i}`));})),(e.added>0||e.overwritten>0)&&(console.log(n.cyan.bold(`
|
|
64
64
|
\u{1F4DD} Next Steps:
|
|
65
|
-
`)),console.log(
|
|
66
|
-
`)),console.log(
|
|
67
|
-
`)),console.log(
|
|
68
|
-
`)),console.log(
|
|
65
|
+
`)),console.log(n.white(" The following has been added to your files:")),console.log(n.gray(" import { seoConfig } from '@/lib/seo';")),console.log(n.gray(" import { JsonLd } from 'seox/next';")),console.log(n.gray(` export const metadata = seoConfig.configToMetadata();
|
|
66
|
+
`)),console.log(n.white(" For layout files, the <JsonLd /> component was also added:")),console.log(n.gray(" <head>")),console.log(n.gray(" <JsonLd seo={seoConfig} />")),console.log(n.gray(` </head>
|
|
67
|
+
`)),console.log(n.white(" To customize metadata for specific pages, you can:")),console.log(n.gray(" 1. Pass overrides to configToMetadata():")),console.log(n.gray(" export const metadata = seoConfig.configToMetadata({")),console.log(n.gray(' title: "Custom Page Title",')),console.log(n.gray(' description: "Custom description"')),console.log(n.gray(` });
|
|
68
|
+
`)),console.log(n.gray(" 2. Add JSON-LD schemas in your seo.ts config")),console.log());}catch(s){o.fail(`Error during configuration. You may have to check your configuration file in ${g(c)}.`),console.error(n.red(s.message)),process.exit(1);}});p.command("doctor").description("Check the validity of your SEO configuration").action(async()=>{console.log(n.cyan.bold(`
|
|
69
69
|
\u{1FA7A} ${m} - SEO Diagnostic
|
|
70
|
-
`));let
|
|
71
|
-
`));return}
|
|
72
|
-
`)),
|
|
73
|
-
`)),
|
|
74
|
-
`)),
|
|
70
|
+
`));let t=j("Analyzing...").start();try{if(!await u.pathExists(g(c))){t.fail(`${c} file not found in ${g(c)}`);return}let s=(await import(g(c))).seoConfig;if(!s||!s.config){t.fail('seoConfig not found in configuration file. Make sure you export "seoConfig".');return}t.stop();let e=b(s.config);if(e.errors.length===0&&e.warnings.length===0){console.log(n.green(`\u2705 No issues detected ! Your SEO configuration is optimal.
|
|
71
|
+
`));return}e.errors.length>0&&(console.log(n.red.bold(`\u274C ERRORS :
|
|
72
|
+
`)),e.errors.map(r=>console.log(n.red(` \u2022 ${r}`))),console.log()),e.warnings.length>0&&(console.log(n.yellow.bold(`\u26A0\uFE0F WARNINGS :
|
|
73
|
+
`)),e.warnings.map(r=>console.log(n.yellow(` \u2022 ${r}`))),console.log()),e.suggestions.length>0&&(console.log(n.cyan.bold(`\u{1F4A1} SUGGESTIONS :
|
|
74
|
+
`)),e.suggestions.map(r=>console.log(n.cyan(` \u2022 ${r}`))),console.log());}catch(o){t.fail("Error during diagnosis"),console.error(n.red(o.message));}});p.parse(process.argv);var $e=p;export{$e as default};
|
package/dist/index.d.cts
CHANGED
|
@@ -1,87 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { OpenGraphType } from 'next/dist/lib/metadata/types/opengraph-types';
|
|
1
|
+
import { Metadata } from 'next';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
interface SEOImage {
|
|
9
|
-
url: string;
|
|
10
|
-
width?: number;
|
|
11
|
-
height?: number;
|
|
12
|
-
alt?: string;
|
|
13
|
-
}
|
|
14
|
-
interface SEORobots {
|
|
15
|
-
index?: boolean;
|
|
16
|
-
follow?: boolean;
|
|
17
|
-
googleBot?: {
|
|
18
|
-
index?: boolean;
|
|
19
|
-
follow?: boolean;
|
|
20
|
-
'max-video-preview'?: number;
|
|
21
|
-
'max-image-preview'?: 'none' | 'standard' | 'large';
|
|
22
|
-
'max-snippet'?: number;
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
interface SEOTwitter {
|
|
26
|
-
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
27
|
-
title?: string;
|
|
28
|
-
description?: string;
|
|
29
|
-
images?: string[];
|
|
30
|
-
creator?: string;
|
|
31
|
-
}
|
|
32
|
-
interface SEOOpenGraph {
|
|
33
|
-
type: OpenGraphType;
|
|
34
|
-
locale?: string;
|
|
35
|
-
url?: string;
|
|
36
|
-
title?: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
siteName?: string;
|
|
39
|
-
images?: SEOImage[];
|
|
40
|
-
}
|
|
41
|
-
interface SEOFormatDetection {
|
|
42
|
-
email?: boolean;
|
|
43
|
-
address?: boolean;
|
|
44
|
-
telephone?: boolean;
|
|
45
|
-
}
|
|
46
|
-
interface SEOIcons {
|
|
47
|
-
icon?: {
|
|
48
|
-
url: string;
|
|
49
|
-
sizes?: string;
|
|
50
|
-
}[];
|
|
51
|
-
apple?: {
|
|
52
|
-
url: string;
|
|
53
|
-
sizes?: string;
|
|
54
|
-
}[];
|
|
55
|
-
shortcut?: {
|
|
56
|
-
url: string;
|
|
57
|
-
sizes?: string;
|
|
58
|
-
}[];
|
|
59
|
-
}
|
|
3
|
+
/**
|
|
4
|
+
* JSON-LD structured data schema
|
|
5
|
+
*/
|
|
60
6
|
interface SEOJsonLd {
|
|
61
7
|
'@context': string;
|
|
62
8
|
'@type': string;
|
|
63
|
-
[key: string]:
|
|
9
|
+
[key: string]: unknown;
|
|
64
10
|
}
|
|
65
11
|
/**
|
|
66
|
-
*
|
|
12
|
+
* SEO configuration extending Next.js Metadata with additional fields
|
|
13
|
+
*
|
|
14
|
+
* - `name`: Site name (used as fallback for openGraph.siteName)
|
|
15
|
+
* - `url`: Site URL (used as fallback for openGraph.url)
|
|
16
|
+
* - `jsonld`: JSON-LD structured data schemas
|
|
67
17
|
*/
|
|
68
|
-
interface SEOConfig {
|
|
18
|
+
interface SEOConfig extends Metadata {
|
|
19
|
+
/** Site name, used as fallback for openGraph.siteName */
|
|
69
20
|
name: string;
|
|
21
|
+
/** Site URL, used as fallback for openGraph.url */
|
|
70
22
|
url: string;
|
|
71
|
-
|
|
72
|
-
description?: string;
|
|
73
|
-
keywords?: string[];
|
|
74
|
-
creator?: string;
|
|
75
|
-
publisher?: string;
|
|
76
|
-
authors?: SEOAuthor[];
|
|
77
|
-
manifest?: string;
|
|
78
|
-
icons?: SEOIcons;
|
|
79
|
-
formatDetection?: SEOFormatDetection;
|
|
80
|
-
openGraph?: SEOOpenGraph;
|
|
81
|
-
twitter?: SEOTwitter;
|
|
82
|
-
robots?: SEORobots;
|
|
23
|
+
/** JSON-LD structured data schemas */
|
|
83
24
|
jsonld?: SEOJsonLd[];
|
|
84
|
-
locale?: string;
|
|
85
25
|
}
|
|
86
26
|
|
|
87
|
-
export type {
|
|
27
|
+
export type { SEOConfig, SEOJsonLd };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,87 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { OpenGraphType } from 'next/dist/lib/metadata/types/opengraph-types';
|
|
1
|
+
import { Metadata } from 'next';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
interface SEOImage {
|
|
9
|
-
url: string;
|
|
10
|
-
width?: number;
|
|
11
|
-
height?: number;
|
|
12
|
-
alt?: string;
|
|
13
|
-
}
|
|
14
|
-
interface SEORobots {
|
|
15
|
-
index?: boolean;
|
|
16
|
-
follow?: boolean;
|
|
17
|
-
googleBot?: {
|
|
18
|
-
index?: boolean;
|
|
19
|
-
follow?: boolean;
|
|
20
|
-
'max-video-preview'?: number;
|
|
21
|
-
'max-image-preview'?: 'none' | 'standard' | 'large';
|
|
22
|
-
'max-snippet'?: number;
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
interface SEOTwitter {
|
|
26
|
-
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
27
|
-
title?: string;
|
|
28
|
-
description?: string;
|
|
29
|
-
images?: string[];
|
|
30
|
-
creator?: string;
|
|
31
|
-
}
|
|
32
|
-
interface SEOOpenGraph {
|
|
33
|
-
type: OpenGraphType;
|
|
34
|
-
locale?: string;
|
|
35
|
-
url?: string;
|
|
36
|
-
title?: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
siteName?: string;
|
|
39
|
-
images?: SEOImage[];
|
|
40
|
-
}
|
|
41
|
-
interface SEOFormatDetection {
|
|
42
|
-
email?: boolean;
|
|
43
|
-
address?: boolean;
|
|
44
|
-
telephone?: boolean;
|
|
45
|
-
}
|
|
46
|
-
interface SEOIcons {
|
|
47
|
-
icon?: {
|
|
48
|
-
url: string;
|
|
49
|
-
sizes?: string;
|
|
50
|
-
}[];
|
|
51
|
-
apple?: {
|
|
52
|
-
url: string;
|
|
53
|
-
sizes?: string;
|
|
54
|
-
}[];
|
|
55
|
-
shortcut?: {
|
|
56
|
-
url: string;
|
|
57
|
-
sizes?: string;
|
|
58
|
-
}[];
|
|
59
|
-
}
|
|
3
|
+
/**
|
|
4
|
+
* JSON-LD structured data schema
|
|
5
|
+
*/
|
|
60
6
|
interface SEOJsonLd {
|
|
61
7
|
'@context': string;
|
|
62
8
|
'@type': string;
|
|
63
|
-
[key: string]:
|
|
9
|
+
[key: string]: unknown;
|
|
64
10
|
}
|
|
65
11
|
/**
|
|
66
|
-
*
|
|
12
|
+
* SEO configuration extending Next.js Metadata with additional fields
|
|
13
|
+
*
|
|
14
|
+
* - `name`: Site name (used as fallback for openGraph.siteName)
|
|
15
|
+
* - `url`: Site URL (used as fallback for openGraph.url)
|
|
16
|
+
* - `jsonld`: JSON-LD structured data schemas
|
|
67
17
|
*/
|
|
68
|
-
interface SEOConfig {
|
|
18
|
+
interface SEOConfig extends Metadata {
|
|
19
|
+
/** Site name, used as fallback for openGraph.siteName */
|
|
69
20
|
name: string;
|
|
21
|
+
/** Site URL, used as fallback for openGraph.url */
|
|
70
22
|
url: string;
|
|
71
|
-
|
|
72
|
-
description?: string;
|
|
73
|
-
keywords?: string[];
|
|
74
|
-
creator?: string;
|
|
75
|
-
publisher?: string;
|
|
76
|
-
authors?: SEOAuthor[];
|
|
77
|
-
manifest?: string;
|
|
78
|
-
icons?: SEOIcons;
|
|
79
|
-
formatDetection?: SEOFormatDetection;
|
|
80
|
-
openGraph?: SEOOpenGraph;
|
|
81
|
-
twitter?: SEOTwitter;
|
|
82
|
-
robots?: SEORobots;
|
|
23
|
+
/** JSON-LD structured data schemas */
|
|
83
24
|
jsonld?: SEOJsonLd[];
|
|
84
|
-
locale?: string;
|
|
85
25
|
}
|
|
86
26
|
|
|
87
|
-
export type {
|
|
27
|
+
export type { SEOConfig, SEOJsonLd };
|
package/dist/next.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
'use strict';var
|
|
1
|
+
'use strict';var r=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 r__namespace=/*#__PURE__*/_interopNamespace(r);var s=class{config;constructor(o){this.config=o;}configToMetadata(o){let{name:t,url:e,jsonld:p,...n}=this.config,{name:c,url:g,jsonld:m,...a}=o??{},d={...n,...a};return (n.openGraph||a.openGraph)&&(d.openGraph={...n.openGraph,...a.openGraph,url:a.openGraph?.url??n.openGraph?.url??e,siteName:a.openGraph?.siteName??n.openGraph?.siteName??t}),d}generatePageMetadata(o){return this.configToMetadata(o)}getJsonLd(o){let t=this.config.jsonld??[],e=o??[];return [...t,...e]}getJsonLdStrings(o){return this.getJsonLd(o).map(t=>JSON.stringify(t))}};function f({seo:i,additionalSchemas:o}){let t=i.getJsonLd(o);return !t||t.length===0?null:r__namespace.createElement(r__namespace.Fragment,null,t.map((e,p)=>r__namespace.createElement("script",{key:`jsonld-${p}`,type:"application/ld+json",dangerouslySetInnerHTML:{__html:JSON.stringify(e)}})))}exports.JsonLd=f;exports.Seox=s;
|
package/dist/next.d.cts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { SEOConfig, SEOJsonLd } from './index.cjs';
|
|
2
2
|
import { Metadata } from 'next';
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import 'next/dist/lib/metadata/types/metadata-types';
|
|
5
|
-
import 'next/dist/lib/metadata/types/opengraph-types';
|
|
6
4
|
|
|
7
5
|
declare class Seox {
|
|
8
6
|
config: SEOConfig;
|
package/dist/next.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { SEOConfig, SEOJsonLd } from './index.js';
|
|
2
2
|
import { Metadata } from 'next';
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import 'next/dist/lib/metadata/types/metadata-types';
|
|
5
|
-
import 'next/dist/lib/metadata/types/opengraph-types';
|
|
6
4
|
|
|
7
5
|
declare class Seox {
|
|
8
6
|
config: SEOConfig;
|
package/dist/next.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as r from'react';var s=class{config;constructor(o){this.config=o;}configToMetadata(o){let{name:t,url:e,jsonld:p,...n}=this.config,{name:c,url:g,jsonld:m,...a}=o??{},d={...n,...a};return (n.openGraph||a.openGraph)&&(d.openGraph={...n.openGraph,...a.openGraph,url:a.openGraph?.url??n.openGraph?.url??e,siteName:a.openGraph?.siteName??n.openGraph?.siteName??t}),d}generatePageMetadata(o){return this.configToMetadata(o)}getJsonLd(o){let t=this.config.jsonld??[],e=o??[];return [...t,...e]}getJsonLdStrings(o){return this.getJsonLd(o).map(t=>JSON.stringify(t))}};function f({seo:i,additionalSchemas:o}){let t=i.getJsonLd(o);return !t||t.length===0?null:r.createElement(r.Fragment,null,t.map((e,p)=>r.createElement("script",{key:`jsonld-${p}`,type:"application/ld+json",dangerouslySetInnerHTML:{__html:JSON.stringify(e)}})))}export{f as JsonLd,s as Seox};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seox",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -52,7 +52,10 @@
|
|
|
52
52
|
],
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@biomejs/biome": "2.2.4",
|
|
55
|
+
"@testing-library/dom": "^10.4.1",
|
|
56
|
+
"@testing-library/react": "^16.3.2",
|
|
55
57
|
"@types/bun": "latest",
|
|
58
|
+
"happy-dom": "^20.3.7",
|
|
56
59
|
"husky": "^9.1.7"
|
|
57
60
|
},
|
|
58
61
|
"dependencies": {
|