seox 0.0.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 ADDED
@@ -0,0 +1,343 @@
1
+ # 🧠 MetaNext
2
+
3
+ > **Simplified SEO management for Next.js App Router**
4
+
5
+ MetaNext 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
+ It combines **TypeScript-typed configuration**, **automatic metadata injection**, and an **intuitive CLI** to guide developers.
7
+
8
+ [![npm version](https://img.shields.io/npm/v/metanext.svg)](https://www.npmjs.com/package/@neysixx/metanext)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
11
+
12
+ ---
13
+
14
+ ## 🎯 Project Goal
15
+
16
+ In a typical Next.js project, each page manually repeats its `<Head>` tags, metadata, and JSON-LD.
17
+ 👉 This creates **duplication**, **inconsistencies**, and complicates maintenance.
18
+
19
+ MetaNext provides:
20
+ - **TypeScript-typed configuration** (`lib/seo.ts`)
21
+ - **Automatic metadata injection** into your Next.js files
22
+ - **Simple API**: `seoConfig.configToMetadata()`
23
+ - **Complete CLI** to initialize and configure your project
24
+
25
+ ---
26
+
27
+ ## 🚀 Quick Start
28
+
29
+ ### 1. Installation
30
+
31
+ ```bash
32
+ # Using Bun (recommended)
33
+ bun i metanext
34
+
35
+ # Using npm
36
+ npm i metanext
37
+
38
+ # Using pnpm
39
+ pnpm i metanext
40
+ ```
41
+
42
+ ### 2. Initialize Configuration
43
+
44
+ ```bash
45
+ # Using Bun (recommended)
46
+ bunx metanext init
47
+
48
+ # Using npx
49
+ npx metanext init
50
+ ```
51
+
52
+ This creates a `lib/seo.ts` file with interactive setup.
53
+
54
+ ### 3. Configure Your Project
55
+
56
+ ```bash
57
+ # Scan and inject metadata into your Next.js files
58
+ bunx metanext configure
59
+ ```
60
+
61
+ ### 4. Verify Configuration (In progress)
62
+
63
+ ```bash
64
+ # Check your SEO configuration
65
+ bunx metanext doctor
66
+ ```
67
+
68
+ ---
69
+
70
+ ## ⚙️ How It Works
71
+
72
+ ### 1. Configuration File: `lib/seo.ts`
73
+
74
+ Created via the `init` command:
75
+
76
+ ```ts
77
+ import { MetaNext } from "metanext/next";
78
+
79
+ export const seoConfig = new MetaNext({
80
+ name: "My Awesome Site",
81
+ url: "https://mysite.com",
82
+ title: {
83
+ default: "My Awesome Site",
84
+ template: "%s | My Awesome Site",
85
+ },
86
+ description: "Welcome to my modern Next.js site",
87
+ keywords: ["nextjs", "seo", "typescript"],
88
+ creator: "Your Name",
89
+ publisher: "Your Company",
90
+ authors: [
91
+ {
92
+ name: "Your Name",
93
+ url: "https://mysite.com/about",
94
+ },
95
+ ],
96
+ openGraph: {
97
+ type: "website",
98
+ locale: "en_US",
99
+ url: "https://mysite.com",
100
+ siteName: "My Awesome Site",
101
+ },
102
+ twitter: {
103
+ card: "summary_large_image",
104
+ creator: "@yourhandle",
105
+ },
106
+ });
107
+ ```
108
+
109
+ 🧠 TypeScript typing guides you through field completion.
110
+ Configuration is **centralized** and **reusable**.
111
+
112
+ ### 2. Automatic Configuration & Injection
113
+
114
+ Once the file is completed:
115
+
116
+ ```bash
117
+ bunx metanext configure
118
+ ```
119
+
120
+ This command:
121
+ - **Scans** your Next.js files (`app/` and `pages/`)
122
+ - **Injects** metadata into your `layout.tsx` and `page.tsx`
123
+ - **Handles** conflicts with existing metadata
124
+ - **Validates** SEO consistency of your configuration
125
+
126
+ ### 3. Usage in Your Pages
127
+
128
+ After running `bunx metanext configure`, your files are automatically updated:
129
+
130
+ ```tsx
131
+ // app/layout.tsx (automatically generated)
132
+ import { seoConfig } from "@/lib/seo";
133
+
134
+ export const metadata = seoConfig.configToMetadata();
135
+ ```
136
+
137
+ #### Page-specific Customization
138
+
139
+ ```tsx
140
+ // app/page.tsx
141
+ import { seoConfig } from "@/lib/seo";
142
+
143
+ export const metadata = seoConfig.configToMetadata({
144
+ title: "Home | My Awesome Site",
145
+ description: "Welcome to our homepage",
146
+ openGraph: {
147
+ title: "Home - My Awesome Site",
148
+ description: "Discover our modern website",
149
+ },
150
+ });
151
+ ```
152
+
153
+ 💡 **Why this approach?**
154
+ - ✅ **Compatible** with all environments (AWS Amplify, Vercel, etc.)
155
+ - ✅ **Predictable** and explicit
156
+ - ✅ **Performant** (direct injection into Next.js metadata)
157
+ - ✅ **Type-safe** with TypeScript
158
+ - ✅ **Automatic** (no need to manually manage each page)
159
+
160
+ 💡 The `configToMetadata()` method:
161
+ - Automatically generates:
162
+ - `<title>` and title templates
163
+ - `<meta name="description">`
164
+ - **OpenGraph** tags
165
+ - **Twitter Card** tags
166
+ - **robots** metadata
167
+ - **JSON-LD** (if configured)
168
+ - Server-side rendering (SSR/SSG) for optimal performance
169
+
170
+ ### 4. Local Overrides
171
+
172
+ Need to modify certain values on the fly?
173
+
174
+ ```tsx
175
+ // app/page.tsx
176
+ import { seoConfig } from "@/lib/seo";
177
+
178
+ export const metadata = seoConfig.configToMetadata({
179
+ title: "Home | Promo 2025",
180
+ description: "New offer available!",
181
+ openGraph: {
182
+ title: "Promo 2025 - My Awesome Site",
183
+ description: "Discover our exceptional offers",
184
+ },
185
+ });
186
+ ```
187
+
188
+ MetaNext merges these fields with the global configuration.
189
+
190
+ ---
191
+
192
+ ## 🧰 Built-in CLI
193
+
194
+ MetaNext provides an intuitive CLI:
195
+
196
+ | Command | Description |
197
+ |---------|-------------|
198
+ | `metanext init` | Creates `lib/seo.ts` with interactive setup |
199
+ | `metanext configure` | Scans and injects metadata into your Next.js files |
200
+ | `metanext doctor` (soon) | Validates your SEO configuration |
201
+
202
+ ### Advanced Options
203
+
204
+ ```bash
205
+ # Force overwrite existing metadata
206
+ bunx metanext configure --force
207
+
208
+ # Validation only (no generation)
209
+ bunx metanext configure --validate
210
+ ```
211
+
212
+ ---
213
+
214
+ ## 🧠 Technical Architecture
215
+
216
+ ```
217
+ [ lib/seo.ts ] ← TypeScript-typed configuration
218
+ ↓ (configure)
219
+ [ Scan Next.js files ] ← automatic detection
220
+
221
+ [ Inject metadata ] ← into layout.tsx/page.tsx
222
+
223
+ [ Server-side rendering ] ← Next.js App Router
224
+ ```
225
+
226
+ ✅ **Centralized configuration** in TypeScript
227
+ ✅ **Automatic injection** into your files
228
+ ✅ **Type-safe** with autocompletion
229
+ ✅ **Optimized SEO** server-side
230
+ ✅ **Simplified maintenance**
231
+
232
+ ---
233
+
234
+ ## 📘 API & Helpers
235
+
236
+ ### `MetaNext` (main class)
237
+ Centralized SEO configuration with complete TypeScript typing.
238
+
239
+ ### `configToMetadata(overrides?)`
240
+ Method that generates Next.js metadata from your configuration.
241
+ Accepts optional overrides to customize per page.
242
+
243
+ ### `seoConfig.configToMetadata()`
244
+ Direct usage in your Next.js files to generate metadata.
245
+
246
+ ---
247
+
248
+ ## 🧭 Roadmap
249
+
250
+ | Feature | Status |
251
+ |---------|--------|
252
+ | TypeScript-typed configuration | ✅ |
253
+ | CLI `init` / `configure` | ✅ |
254
+ | Automatic metadata injection | ✅ |
255
+ | OpenGraph and Twitter Cards support | ✅ |
256
+ | Local overrides per page | ✅ |
257
+ | SEO validation with `doctor` | ✅ |
258
+ | CLI `doctor` | 🔜 |
259
+ | Multilingual support (`hreflang`) | 🔜 |
260
+ | Automatic OG image generation | 🔜 |
261
+ | Predefined templates (`--template blog`) | 🔜 |
262
+ | Advanced JSON-LD support | 🔜 |
263
+
264
+ ---
265
+
266
+ ## 📦 Installation
267
+
268
+ ### Requirements
269
+
270
+ - **Node.js** >= 18.0.0
271
+ - **Next.js** >= 14.0.0
272
+ - **React** >= 18.0.0
273
+ - **TypeScript** >= 5.9.3
274
+
275
+ ### Package Managers
276
+
277
+ ```bash
278
+ # Bun (recommended)
279
+ bun add metanext
280
+
281
+ # npm
282
+ npm install metanext
283
+
284
+ # pnpm
285
+ pnpm add metanext
286
+
287
+ # yarn
288
+ yarn add metanext
289
+ ```
290
+
291
+ ---
292
+
293
+ ## 🤝 Contributing
294
+
295
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
296
+
297
+ ### Development Setup
298
+
299
+ ```bash
300
+ # Clone the repository
301
+ git clone https://github.com/neysixx/metanext.git
302
+ cd metanext
303
+
304
+ # Install dependencies
305
+ bun install
306
+
307
+ # Build the project
308
+ bun run build
309
+ ```
310
+
311
+ ---
312
+
313
+ ## 📜 License
314
+
315
+ MIT © 2025 — Designed for modern Next.js developers 🧑‍💻
316
+
317
+ ---
318
+
319
+ ## 🙏 Acknowledgments
320
+
321
+ - Built with [Next.js](https://nextjs.org/)
322
+ - Powered by [TypeScript](https://www.typescriptlang.org/)
323
+ - CLI built with [Commander.js](https://github.com/tj/commander.js)
324
+ - Styling with [Chalk](https://github.com/chalk/chalk)
325
+
326
+ ---
327
+
328
+ ## 📞 Support
329
+
330
+ - 📖 [Documentation](https://github.com/neysixx/metanext#readme)
331
+ - 🐛 [Report Issues](https://github.com/neysixx/metanext/issues)
332
+ - 💬 [Discussions](https://github.com/neysixx/metanext/discussions)
333
+ - 📧 [Email](mailto:kylliansenrens3004@gmail.com)
334
+
335
+ ---
336
+
337
+ <div align="center">
338
+
339
+ **Made with ❤️ for the Next.js community**
340
+
341
+ [⭐ Star us on GitHub](https://github.com/neysixx/metanext) • [🐦 Follow on X](https://x.com/ks_nsx) • [📧 Contact](mailto:kylliansenrens3004@gmail.com)
342
+
343
+ </div>
package/dist/cli.cjs ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';var commander=require('commander'),t=require('chalk'),m=require('fs-extra'),I=require('ora'),y=require('path'),v=require('prompts');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var t__default=/*#__PURE__*/_interopDefault(t);var m__default=/*#__PURE__*/_interopDefault(m);var I__default=/*#__PURE__*/_interopDefault(I);var y__default=/*#__PURE__*/_interopDefault(y);var v__default=/*#__PURE__*/_interopDefault(v);var p=new commander.Command().name("metanext").description("\u{1F9E0} MetaNext - Simplified SEO management for Next.js App Router").version("1.0.0");var $="SEOX";var c="seo.ts";var b={config:`import { MetaNext } from "metanext/next";
3
+
4
+ export const seoConfig = new MetaNext({
5
+ name: "{{siteName}}",
6
+ url: "{{baseUrl}}",
7
+ title: {
8
+ default: "{{siteName}}",
9
+ template: "",
10
+ },
11
+ description: "{{siteDescription}}",
12
+ keywords: [],
13
+ creator: "",
14
+ publisher: "",
15
+ authors: [
16
+ {
17
+ name: "",
18
+ url: "",
19
+ },
20
+ ],
21
+ manifest: "",
22
+ })`};function g(a){return m__default.default.existsSync(process.cwd()+"/src")?y__default.default.join(process.cwd(),"src","lib",a):y__default.default.join(process.cwd(),"lib",a)}function O(a){let r={errors:[],warnings:[],suggestions:[]};return Object.entries(a.pages||{}).forEach(([o,e])=>{e.title?e.title.length>60&&r.warnings.push(`Page "${o}" : title too long (${e.title.length} > 60)`):r.errors.push(`Page "${o}" : title missing`),e.description?e.description.length>160&&r.warnings.push(`Page "${o}" : description too long (${e.description.length} > 160)`):r.errors.push(`Page "${o}" : description missing`),(!e.jsonld||e.jsonld.length===0)&&r.suggestions.push(`Page "${o}" : add JSON-LD to improve indexing`);}),r}async function S(){let a=[],r=m__default.default.existsSync(process.cwd()+"/src")?"src":"",o=y__default.default.join(process.cwd(),r,"app"),e=y__default.default.join(process.cwd(),r,"pages");return await m__default.default.pathExists(o)&&await E(o,a),await m__default.default.pathExists(e)&&await E(e,a),a}async function E(a,r,o){let e=await m__default.default.readdir(a,{withFileTypes:true});for(let s of e){let i=y__default.default.join(a,s.name);if(s.isDirectory())await E(i,r);else if(s.isFile()&&(s.name==="layout.tsx"||s.name==="page.tsx"||s.name==="layout.ts"||s.name==="page.ts")){let n=await m__default.default.readFile(i,"utf8"),l=n.includes("export const metadata"),f=l?F(n):void 0;r.push({path:i,hasMetadata:l,metadataContent:f});}}}function F(a){let r=a.split(`
23
+ `),o=[],e=false,s=0;for(let i of r){if(i.includes("export const metadata")){e=true,o.push(i),s=(i.match(/\{/g)||[]).length-(i.match(/\}/g)||[]).length;continue}if(e&&(o.push(i),s+=(i.match(/\{/g)||[]).length-(i.match(/\}/g)||[]).length,s===0))break}return o.join(`
24
+ `)}async function w(a,r=false){let o=await m__default.default.readFile(a,"utf8");if(o.includes("export const metadata")&&!r)return false;let e=o;if(r&&o.includes("export const metadata")&&(e=o.replace(/export const metadata[^;]+;?\s*/gs,"")),!e.includes("import { seoConfig } from '@/lib/seo'")){let l=`import { seoConfig } from '@/lib/seo';
25
+ `,f=e.split(`
26
+ `),x=0;for(let h=0;h<f.length;h++)if(f[h].startsWith("import "))x=h+1;else if(f[h].trim()===""&&x>0)break;f.splice(x,0,l),e=f.join(`
27
+ `);}let s=`
28
+ export const metadata = seoConfig.configToMetadata();
29
+ `,i=e.split(`
30
+ `),n=i.length;for(let l=0;l<i.length;l++)if(i[l].startsWith("export default")||i[l].startsWith("export function")||i[l].startsWith("export const")&&!i[l].includes("metadata")){n=l;break}return i.splice(n,0,s),e=i.join(`
31
+ `),await m__default.default.writeFile(a,e,"utf8"),true}p.command("init").description("Initialize the SEOX SEO configuration").action(async a=>{if(console.log(t__default.default.cyan.bold(`
32
+ \u{1F9E0} MetaNext - Initialization
33
+ `)),await m__default.default.pathExists(g(c))){let{overwrite:e}=await v__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(t__default.default.yellow("\u26A0\uFE0F Initialization canceled"));return}}let r=await v__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(!r.siteName){console.log(t__default.default.red("\u274C Initialization canceled"));return}let o=I__default.default("Creating files...").start();try{await m__default.default.ensureDir(y__default.default.join(process.cwd(),"lib"));let e=b.config;Object.entries(r).forEach(([s,i])=>{e=e.replace(new RegExp(`{{${s}}}`,"g"),i);}),await m__default.default.writeFile(g(c),e,"utf8"),o.succeed("Configuration created successfully!"),console.log(t__default.default.green(`
34
+ \u2705 File created : `)+t__default.default.gray(g(c))),console.log(t__default.default.cyan(`
35
+ \u{1F4DD} Next step :`)),console.log(t__default.default.white(" bunx metanext configure")),console.log();}catch(e){o.fail("Error creating the configuration"),console.error(t__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 a=>{console.log(t__default.default.cyan.bold(`
36
+ \u2699\uFE0F ${$} - Configuration
37
+ `));let r=I__default.default("Reading configuration...").start();try{if(!await m__default.default.pathExists(g(c))){r.fail(c+" file not found"),console.log(t__default.default.yellow(`
38
+ \u{1F4A1} Run first: `)+t__default.default.white("bunx seox init"));return}r.text="Scanning Next.js files...";let o=await S();if(o.length===0){r.fail("No Next.js layout or page files found"),console.log(t__default.default.yellow(`
39
+ \u{1F4A1} Make sure you have an app/ or pages/ directory with layout.tsx/page.tsx files`));return}r.succeed(`Found ${o.length} file(s) to process`),console.log(t__default.default.cyan(`
40
+ \u{1F4C1} Files found:`)),o.forEach(n=>{let l=n.hasMetadata?t__default.default.yellow("(has metadata)"):t__default.default.green("(no metadata)");console.log(t__default.default.gray(` \u2022 ${n.path} ${l}`));});let e={added:0,overwritten:0,skipped:0,errors:[]},s=o.filter(n=>n.hasMetadata),i=o.filter(n=>!n.hasMetadata);console.log(t__default.default.cyan(`
41
+ \u{1F504} Processing files without metadata...`));for(let n of i)try{await w(n.path,!1)&&(e.added++,console.log(t__default.default.green(` \u2713 Added metadata to ${n.path}`)));}catch(l){e.errors.push(`${n.path}: ${l.message}`),console.log(t__default.default.red(` \u2717 Error in ${n.path}`));}if(s.length>0)if(console.log(t__default.default.yellow(`
42
+ \u26A0\uFE0F ${s.length} file(s) already have metadata exports`)),a.force){console.log(t__default.default.cyan(`
43
+ \u{1F504} Overwriting all existing metadata (--force)...`));for(let n of s)try{await w(n.path,!0)&&(e.overwritten++,console.log(t__default.default.yellow(` \u2713 Overwritten ${n.path}`)));}catch(l){e.errors.push(`${n.path}: ${l.message}`),console.log(t__default.default.red(` \u2717 Error in ${n.path}`));}}else {console.log(t__default.default.cyan(`
44
+ \u{1F504} Processing files with existing metadata...`));for(let n of s){let l=await v__default.default({type:"confirm",name:"overwrite",message:`Overwrite metadata in ${n.path}?`,initial:!1});if(l.overwrite===void 0){console.log(t__default.default.yellow(`
45
+ \u274C Operation cancelled`));break}if(l.overwrite)try{await w(n.path,!0)&&(e.overwritten++,console.log(t__default.default.yellow(` \u2713 Overwritten ${n.path}`)));}catch(f){e.errors.push(`${n.path}: ${f.message}`),console.log(t__default.default.red(` \u2717 Error in ${n.path}`));}else e.skipped++,console.log(t__default.default.gray(` \u25CB Skipped ${n.path}`));}}console.log(t__default.default.green.bold(`
46
+ \u2705 Configuration Summary:
47
+ `)),console.log(t__default.default.gray(` \u2022 ${e.added} file(s) with metadata added`)),e.overwritten>0&&console.log(t__default.default.yellow(` \u2022 ${e.overwritten} file(s) overwritten`)),e.skipped>0&&console.log(t__default.default.gray(` \u2022 ${e.skipped} file(s) skipped`)),e.errors.length>0&&(console.log(t__default.default.red(` \u2022 ${e.errors.length} error(s):`)),e.errors.forEach(n=>{console.log(t__default.default.red(` - ${n}`));})),(e.added>0||e.overwritten>0)&&(console.log(t__default.default.cyan.bold(`
48
+ \u{1F4DD} Next Steps:
49
+ `)),console.log(t__default.default.white(" The following metadata export has been added to your files:")),console.log(t__default.default.gray(" import { seoConfig } from '@/lib/seo';")),console.log(t__default.default.gray(` export const metadata = seoConfig.configToMetadata();
50
+ `)),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(` });
51
+ `)),console.log(t__default.default.gray(" 2. Or manually modify the metadata object after generation")),console.log());}catch(o){r.fail(`Error during configuration. You may have to check your configuration file in ${g(c)}.`),console.error(t__default.default.red(o.message)),process.exit(1);}});p.command("doctor").description("Check the validity of your SEO configuration").action(async()=>{console.log(t__default.default.cyan.bold(`
52
+ \u{1FA7A} MetaNext - Diagnostic SEO
53
+ `));let a=I__default.default("Analyzing...").start();try{if(!await m__default.default.pathExists(g(c))){a.fail(c+" file not found");return}let o=(await import(g(c))).default;a.stop();let e=O(o);if(e.errors.length===0&&e.warnings.length===0){console.log(t__default.default.green(`\u2705 No issues detected ! Your SEO configuration is optimal.
54
+ `));return}e.errors.length>0&&(console.log(t__default.default.red.bold(`\u274C ERRORS :
55
+ `)),e.errors.map(s=>console.log(t__default.default.red(` \u2022 ${s}`))),console.log()),e.warnings.length>0&&(console.log(t__default.default.yellow.bold(`\u26A0\uFE0F WARNINGS :
56
+ `)),e.warnings.map(s=>console.log(t__default.default.yellow(` \u2022 ${s}`))),console.log()),e.suggestions.length>0&&(console.log(t__default.default.cyan.bold(`\u{1F4A1} SUGGESTIONS :
57
+ `)),e.suggestions.map(s=>console.log(t__default.default.cyan(` \u2022 ${s}`))),console.log());}catch(r){a.fail("Error during diagnosis"),console.error(t__default.default.red(r.message));}});p.parse(process.argv);var ye=p;module.exports=ye;
package/dist/cli.d.cts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+
4
+ declare const program: Command;
5
+
6
+ export { program as default };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+
4
+ declare const program: Command;
5
+
6
+ export { program as default };
package/dist/cli.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import {Command}from'commander';import t from'chalk';import m from'fs-extra';import I from'ora';import y from'path';import v from'prompts';var p=new Command().name("metanext").description("\u{1F9E0} MetaNext - Simplified SEO management for Next.js App Router").version("1.0.0");var $="SEOX";var c="seo.ts";var b={config:`import { MetaNext } from "metanext/next";
3
+
4
+ export const seoConfig = new MetaNext({
5
+ name: "{{siteName}}",
6
+ url: "{{baseUrl}}",
7
+ title: {
8
+ default: "{{siteName}}",
9
+ template: "",
10
+ },
11
+ description: "{{siteDescription}}",
12
+ keywords: [],
13
+ creator: "",
14
+ publisher: "",
15
+ authors: [
16
+ {
17
+ name: "",
18
+ url: "",
19
+ },
20
+ ],
21
+ manifest: "",
22
+ })`};function g(a){return m.existsSync(process.cwd()+"/src")?y.join(process.cwd(),"src","lib",a):y.join(process.cwd(),"lib",a)}function O(a){let r={errors:[],warnings:[],suggestions:[]};return Object.entries(a.pages||{}).forEach(([o,e])=>{e.title?e.title.length>60&&r.warnings.push(`Page "${o}" : title too long (${e.title.length} > 60)`):r.errors.push(`Page "${o}" : title missing`),e.description?e.description.length>160&&r.warnings.push(`Page "${o}" : description too long (${e.description.length} > 160)`):r.errors.push(`Page "${o}" : description missing`),(!e.jsonld||e.jsonld.length===0)&&r.suggestions.push(`Page "${o}" : add JSON-LD to improve indexing`);}),r}async function S(){let a=[],r=m.existsSync(process.cwd()+"/src")?"src":"",o=y.join(process.cwd(),r,"app"),e=y.join(process.cwd(),r,"pages");return await m.pathExists(o)&&await E(o,a),await m.pathExists(e)&&await E(e,a),a}async function E(a,r,o){let e=await m.readdir(a,{withFileTypes:true});for(let s of e){let i=y.join(a,s.name);if(s.isDirectory())await E(i,r);else if(s.isFile()&&(s.name==="layout.tsx"||s.name==="page.tsx"||s.name==="layout.ts"||s.name==="page.ts")){let n=await m.readFile(i,"utf8"),l=n.includes("export const metadata"),f=l?F(n):void 0;r.push({path:i,hasMetadata:l,metadataContent:f});}}}function F(a){let r=a.split(`
23
+ `),o=[],e=false,s=0;for(let i of r){if(i.includes("export const metadata")){e=true,o.push(i),s=(i.match(/\{/g)||[]).length-(i.match(/\}/g)||[]).length;continue}if(e&&(o.push(i),s+=(i.match(/\{/g)||[]).length-(i.match(/\}/g)||[]).length,s===0))break}return o.join(`
24
+ `)}async function w(a,r=false){let o=await m.readFile(a,"utf8");if(o.includes("export const metadata")&&!r)return false;let e=o;if(r&&o.includes("export const metadata")&&(e=o.replace(/export const metadata[^;]+;?\s*/gs,"")),!e.includes("import { seoConfig } from '@/lib/seo'")){let l=`import { seoConfig } from '@/lib/seo';
25
+ `,f=e.split(`
26
+ `),x=0;for(let h=0;h<f.length;h++)if(f[h].startsWith("import "))x=h+1;else if(f[h].trim()===""&&x>0)break;f.splice(x,0,l),e=f.join(`
27
+ `);}let s=`
28
+ export const metadata = seoConfig.configToMetadata();
29
+ `,i=e.split(`
30
+ `),n=i.length;for(let l=0;l<i.length;l++)if(i[l].startsWith("export default")||i[l].startsWith("export function")||i[l].startsWith("export const")&&!i[l].includes("metadata")){n=l;break}return i.splice(n,0,s),e=i.join(`
31
+ `),await m.writeFile(a,e,"utf8"),true}p.command("init").description("Initialize the SEOX SEO configuration").action(async a=>{if(console.log(t.cyan.bold(`
32
+ \u{1F9E0} MetaNext - Initialization
33
+ `)),await m.pathExists(g(c))){let{overwrite:e}=await v({type:"confirm",name:"overwrite",message:`The ${g(c)} file already exists. Do you want to overwrite it?`,initial:false});if(!e){console.log(t.yellow("\u26A0\uFE0F Initialization canceled"));return}}let r=await v([{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(!r.siteName){console.log(t.red("\u274C Initialization canceled"));return}let o=I("Creating files...").start();try{await m.ensureDir(y.join(process.cwd(),"lib"));let e=b.config;Object.entries(r).forEach(([s,i])=>{e=e.replace(new RegExp(`{{${s}}}`,"g"),i);}),await m.writeFile(g(c),e,"utf8"),o.succeed("Configuration created successfully!"),console.log(t.green(`
34
+ \u2705 File created : `)+t.gray(g(c))),console.log(t.cyan(`
35
+ \u{1F4DD} Next step :`)),console.log(t.white(" bunx metanext configure")),console.log();}catch(e){o.fail("Error creating the configuration"),console.error(t.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 a=>{console.log(t.cyan.bold(`
36
+ \u2699\uFE0F ${$} - Configuration
37
+ `));let r=I("Reading configuration...").start();try{if(!await m.pathExists(g(c))){r.fail(c+" file not found"),console.log(t.yellow(`
38
+ \u{1F4A1} Run first: `)+t.white("bunx seox init"));return}r.text="Scanning Next.js files...";let o=await S();if(o.length===0){r.fail("No Next.js layout or page files found"),console.log(t.yellow(`
39
+ \u{1F4A1} Make sure you have an app/ or pages/ directory with layout.tsx/page.tsx files`));return}r.succeed(`Found ${o.length} file(s) to process`),console.log(t.cyan(`
40
+ \u{1F4C1} Files found:`)),o.forEach(n=>{let l=n.hasMetadata?t.yellow("(has metadata)"):t.green("(no metadata)");console.log(t.gray(` \u2022 ${n.path} ${l}`));});let e={added:0,overwritten:0,skipped:0,errors:[]},s=o.filter(n=>n.hasMetadata),i=o.filter(n=>!n.hasMetadata);console.log(t.cyan(`
41
+ \u{1F504} Processing files without metadata...`));for(let n of i)try{await w(n.path,!1)&&(e.added++,console.log(t.green(` \u2713 Added metadata to ${n.path}`)));}catch(l){e.errors.push(`${n.path}: ${l.message}`),console.log(t.red(` \u2717 Error in ${n.path}`));}if(s.length>0)if(console.log(t.yellow(`
42
+ \u26A0\uFE0F ${s.length} file(s) already have metadata exports`)),a.force){console.log(t.cyan(`
43
+ \u{1F504} Overwriting all existing metadata (--force)...`));for(let n of s)try{await w(n.path,!0)&&(e.overwritten++,console.log(t.yellow(` \u2713 Overwritten ${n.path}`)));}catch(l){e.errors.push(`${n.path}: ${l.message}`),console.log(t.red(` \u2717 Error in ${n.path}`));}}else {console.log(t.cyan(`
44
+ \u{1F504} Processing files with existing metadata...`));for(let n of s){let l=await v({type:"confirm",name:"overwrite",message:`Overwrite metadata in ${n.path}?`,initial:!1});if(l.overwrite===void 0){console.log(t.yellow(`
45
+ \u274C Operation cancelled`));break}if(l.overwrite)try{await w(n.path,!0)&&(e.overwritten++,console.log(t.yellow(` \u2713 Overwritten ${n.path}`)));}catch(f){e.errors.push(`${n.path}: ${f.message}`),console.log(t.red(` \u2717 Error in ${n.path}`));}else e.skipped++,console.log(t.gray(` \u25CB Skipped ${n.path}`));}}console.log(t.green.bold(`
46
+ \u2705 Configuration Summary:
47
+ `)),console.log(t.gray(` \u2022 ${e.added} file(s) with metadata added`)),e.overwritten>0&&console.log(t.yellow(` \u2022 ${e.overwritten} file(s) overwritten`)),e.skipped>0&&console.log(t.gray(` \u2022 ${e.skipped} file(s) skipped`)),e.errors.length>0&&(console.log(t.red(` \u2022 ${e.errors.length} error(s):`)),e.errors.forEach(n=>{console.log(t.red(` - ${n}`));})),(e.added>0||e.overwritten>0)&&(console.log(t.cyan.bold(`
48
+ \u{1F4DD} Next Steps:
49
+ `)),console.log(t.white(" The following metadata export has been added to your files:")),console.log(t.gray(" import { seoConfig } from '@/lib/seo';")),console.log(t.gray(` export const metadata = seoConfig.configToMetadata();
50
+ `)),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(` });
51
+ `)),console.log(t.gray(" 2. Or manually modify the metadata object after generation")),console.log());}catch(o){r.fail(`Error during configuration. You may have to check your configuration file in ${g(c)}.`),console.error(t.red(o.message)),process.exit(1);}});p.command("doctor").description("Check the validity of your SEO configuration").action(async()=>{console.log(t.cyan.bold(`
52
+ \u{1FA7A} MetaNext - Diagnostic SEO
53
+ `));let a=I("Analyzing...").start();try{if(!await m.pathExists(g(c))){a.fail(c+" file not found");return}let o=(await import(g(c))).default;a.stop();let e=O(o);if(e.errors.length===0&&e.warnings.length===0){console.log(t.green(`\u2705 No issues detected ! Your SEO configuration is optimal.
54
+ `));return}e.errors.length>0&&(console.log(t.red.bold(`\u274C ERRORS :
55
+ `)),e.errors.map(s=>console.log(t.red(` \u2022 ${s}`))),console.log()),e.warnings.length>0&&(console.log(t.yellow.bold(`\u26A0\uFE0F WARNINGS :
56
+ `)),e.warnings.map(s=>console.log(t.yellow(` \u2022 ${s}`))),console.log()),e.suggestions.length>0&&(console.log(t.cyan.bold(`\u{1F4A1} SUGGESTIONS :
57
+ `)),e.suggestions.map(s=>console.log(t.cyan(` \u2022 ${s}`))),console.log());}catch(r){a.fail("Error during diagnosis"),console.error(t.red(r.message));}});p.parse(process.argv);var ye=p;export{ye as default};
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ 'use strict';
@@ -0,0 +1,87 @@
1
+ import { TemplateString } from 'next/dist/lib/metadata/types/metadata-types';
2
+ import { OpenGraphType } from 'next/dist/lib/metadata/types/opengraph-types';
3
+
4
+ interface SEOAuthor {
5
+ name: string;
6
+ url?: string;
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
+ }
60
+ interface SEOJsonLd {
61
+ '@context': string;
62
+ '@type': string;
63
+ [key: string]: any;
64
+ }
65
+ /**
66
+ * Main SEO structure for site or page-level configuration
67
+ */
68
+ interface SEOConfig {
69
+ name: string;
70
+ url: string;
71
+ title?: TemplateString | string;
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;
83
+ jsonld?: SEOJsonLd[];
84
+ locale?: string;
85
+ }
86
+
87
+ export type { SEOAuthor, SEOConfig, SEOFormatDetection, SEOIcons, SEOImage, SEOJsonLd, SEOOpenGraph, SEORobots, SEOTwitter };
@@ -0,0 +1,87 @@
1
+ import { TemplateString } from 'next/dist/lib/metadata/types/metadata-types';
2
+ import { OpenGraphType } from 'next/dist/lib/metadata/types/opengraph-types';
3
+
4
+ interface SEOAuthor {
5
+ name: string;
6
+ url?: string;
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
+ }
60
+ interface SEOJsonLd {
61
+ '@context': string;
62
+ '@type': string;
63
+ [key: string]: any;
64
+ }
65
+ /**
66
+ * Main SEO structure for site or page-level configuration
67
+ */
68
+ interface SEOConfig {
69
+ name: string;
70
+ url: string;
71
+ title?: TemplateString | string;
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;
83
+ jsonld?: SEOJsonLd[];
84
+ locale?: string;
85
+ }
86
+
87
+ export type { SEOAuthor, SEOConfig, SEOFormatDetection, SEOIcons, SEOImage, SEOJsonLd, SEOOpenGraph, SEORobots, SEOTwitter };
package/dist/index.js ADDED
File without changes
package/dist/next.cjs ADDED
@@ -0,0 +1 @@
1
+ 'use strict';var n=class{config;constructor(t){this.config=t;}configToMetadata(t){let o={...this.config,...t},i={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(i).filter(([e,a])=>a!==void 0))}generatePageMetadata(t){return this.configToMetadata(t)}};exports.MetaNext=n;
@@ -0,0 +1,13 @@
1
+ import { SEOConfig } from './index.cjs';
2
+ import { Metadata } from 'next';
3
+ import 'next/dist/lib/metadata/types/metadata-types';
4
+ import 'next/dist/lib/metadata/types/opengraph-types';
5
+
6
+ declare class MetaNext {
7
+ private config;
8
+ constructor(config: SEOConfig);
9
+ configToMetadata(overrides?: Partial<SEOConfig>): Metadata;
10
+ generatePageMetadata(overrides?: Partial<SEOConfig>): Metadata;
11
+ }
12
+
13
+ export { MetaNext, SEOConfig };
package/dist/next.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { SEOConfig } from './index.js';
2
+ import { Metadata } from 'next';
3
+ import 'next/dist/lib/metadata/types/metadata-types';
4
+ import 'next/dist/lib/metadata/types/opengraph-types';
5
+
6
+ declare class MetaNext {
7
+ private config;
8
+ constructor(config: SEOConfig);
9
+ configToMetadata(overrides?: Partial<SEOConfig>): Metadata;
10
+ generatePageMetadata(overrides?: Partial<SEOConfig>): Metadata;
11
+ }
12
+
13
+ export { MetaNext, SEOConfig };
package/dist/next.js ADDED
@@ -0,0 +1 @@
1
+ var n=class{config;constructor(t){this.config=t;}configToMetadata(t){let o={...this.config,...t},i={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(i).filter(([e,a])=>a!==void 0))}generatePageMetadata(t){return this.configToMetadata(t)}};export{n as MetaNext};
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "seox",
3
+ "version": "0.0.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "bin": {
13
+ "seox": "./dist/cli.js"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ },
21
+ "./next": {
22
+ "types": "./dist/next.d.ts",
23
+ "import": "./dist/next.js",
24
+ "require": "./dist/next.cjs"
25
+ }
26
+ },
27
+ "type": "module",
28
+ "author": "Kyllian SENRENS <kylliansenrens3004@gmail.com>",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/neysixx/seox.git"
32
+ },
33
+ "scripts": {
34
+ "prepare": "husky",
35
+ "format:check": "biome check",
36
+ "format:fix": "biome check --write",
37
+ "build": "NODE_ENV=production tsup",
38
+ "build:dev": "NODE_ENV=development tsup",
39
+ "build:watch": "NODE_ENV=development tsup --watch",
40
+ "build:analyze": "ANALYZE=true NODE_ENV=production tsup",
41
+ "analyze": "node scripts/analyze-build.js",
42
+ "dev": "NODE_ENV=development tsup --watch",
43
+ "test": "bun test",
44
+ "typecheck": "tsc --noEmit",
45
+ "prepublishOnly": "bun run build && bun run test",
46
+ "release": "bun run build && changeset publish"
47
+ },
48
+ "keywords": [
49
+ "nextjs",
50
+ "seo",
51
+ "typescript"
52
+ ],
53
+ "devDependencies": {
54
+ "@biomejs/biome": "2.2.4",
55
+ "@types/bun": "latest",
56
+ "husky": "^9.1.7"
57
+ },
58
+ "dependencies": {
59
+ "@semantic-release/changelog": "^6.0.3",
60
+ "@semantic-release/git": "^10.0.1",
61
+ "@types/fs-extra": "^11.0.4",
62
+ "@types/prompts": "^2.4.9",
63
+ "chalk": "^5.3.0",
64
+ "commander": "^11.1.0",
65
+ "fs-extra": "^11.2.0",
66
+ "install": "^0.13.0",
67
+ "ora": "^7.0.1",
68
+ "prompts": "^2.4.2",
69
+ "semantic-release": "^24.2.9",
70
+ "tsup": "^8.5.0"
71
+ },
72
+ "peerDependencies": {
73
+ "typescript": "^5.9.3",
74
+ "next": ">=14.0.0",
75
+ "react": ">=18.0.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "next": {
79
+ "optional": false
80
+ },
81
+ "react": {
82
+ "optional": false
83
+ }
84
+ },
85
+ "engines": {
86
+ "node": ">=18.0.0",
87
+ "bun": ">=1.0.0"
88
+ }
89
+ }