@vijayhardaha/next-indexnow 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vijay Hardaha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # @vijayhardaha/next-indexnow
2
+
3
+ CLI tool to validate a Next.js project and submit sitemap URLs to the IndexNow API for faster search engine indexing.
4
+
5
+ **Problem**: You have a Next.js site with a generated sitemap, but search engines take time to discover and index your new or updated pages.
6
+
7
+ **Solution**: `next-indexnow` validates your project environment, reads the sitemap XML, extracts all URLs, and submits them to the IndexNow API in batches — instantly notifying search engines about your content changes.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @vijayhardaha/next-indexnow
13
+ # or
14
+ bun add @vijayhardaha/next-indexnow
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ # Run from your Next.js project root — auto-detects settings from next-sitemap.config
21
+ npx next-indexnow
22
+
23
+ # Override the site URL
24
+ npx next-indexnow --site-url https://example.com
25
+
26
+ # Provide IndexNow API key
27
+ npx next-indexnow --key my-api-key
28
+
29
+ # Use a custom sitemap path
30
+ npx next-indexnow --sitemap ./public/sitemap.xml
31
+
32
+ # Preview URLs without submitting
33
+ npx next-indexnow --dry-run
34
+ ```
35
+
36
+ The tool reads your sitemap, parses the URLs, and submits them to `https://api.indexnow.org/indexnow` in chunks.
37
+
38
+ ## CLI Output
39
+
40
+ ```
41
+ ██╗███╗ ██╗██████╗ ███████╗██╗ ██╗███╗ ██╗ ██████╗ ██╗ ██╗
42
+ ██║████╗ ██║██╔══██╗██╔════╝╚██╗██╔╝████╗ ██║██╔═══██╗██║ ██║
43
+ ██║██╔██╗ ██║██║ ██║█████╗ ╚███╔╝ ██╔██╗ ██║██║ ██║██║ █╗ ██║
44
+ ██║██║╚██╗██║██║ ██║██╔══╝ ██╔██╗ ██║╚██╗██║██║ ██║██║███╗██║
45
+ ██║██║ ╚████║██████╔╝███████╗██╔╝ ██╗██║ ╚████║╚██████╔╝╚███╔███╔╝
46
+ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══╝╚══╝
47
+
48
+ =====================================================================
49
+ next-indexnow: v0.0.1
50
+ =====================================================================
51
+
52
+ ✔ Next.js config found
53
+ ✔ Build directory exists (.next)
54
+ ✔ Sitemap config found: ./next-sitemap.config.js
55
+ ✔ Site URL resolved: example.com
56
+ ✔ API key resolved: using built-in default key
57
+ ✔ Key verification file: 91c8...txt created
58
+ ✔ Sitemap loaded: 10 URLs found
59
+
60
+ =====================================================================
61
+ ✔ Submission Completed 🎉
62
+ =====================================================================
63
+
64
+ ✔ URLs found: 10
65
+ ✔ URLs submitted: 10
66
+ ✔ URLs failed: 0
67
+ ✔ Duration: 1.23s
68
+
69
+ All 10 urls submitted to IndexNow successfully! 🥳
70
+ ```
71
+
72
+ ## Options
73
+
74
+ | Option | Description |
75
+ | ----------------------- | --------------------------------------------------------------------------------- |
76
+ | `--site-url <url>` | Site URL (e.g. `https://example.com`). Overrides config value |
77
+ | `--key <key>` | IndexNow API key. Falls back to `INDEXNOW_KEY` env variable or a built-in default |
78
+ | `--sitemap <path>` | Path to the sitemap XML file |
79
+ | `--chunk-size <number>` | URLs per submission batch (default: 100) |
80
+ | `-d, --dry-run` | Preview URLs without submitting to the IndexNow API |
81
+ | `-h, --help` | Show help |
82
+ | `--version` | Show version |
83
+
84
+ ## How It Works
85
+
86
+ 1. **Validates** your Next.js project (`next.config.*` must exist)
87
+ 2. **Checks** the `.next` build directory exists and is not empty
88
+ 3. **Reads** `next-sitemap.config.*` to extract `siteUrl` and `outDir`
89
+ 4. **Resolves** the API key from CLI option, `INDEXNOW_KEY` env variable, or a built-in default key
90
+ 5. **Creates** the IndexNow verification file at `public/<key>.txt`
91
+ 6. **Parses** all `<loc>` URLs from the sitemap XML
92
+ 7. **Submits** URLs in batches (default 100) to the IndexNow API
93
+ 8. **Reports** results with per-chunk success/failure details
94
+
95
+ ### Environment Variables
96
+
97
+ | Variable | Description |
98
+ | -------------- | ------------------------------------------------------------ |
99
+ | `INDEXNOW_KEY` | IndexNow API key (optional — falls back to built-in default) |
100
+
101
+ ## Programmatic API
102
+
103
+ ```typescript
104
+ import { run } from "@vijayhardaha/next-indexnow";
105
+
106
+ const result = await run({
107
+ siteUrl: "https://example.com",
108
+ key: "my-api-key",
109
+ dryRun: true // preview only
110
+ });
111
+
112
+ console.log(`Found ${result.urlsFound} URLs`);
113
+ ```
114
+
115
+ ### `NextIndexnowOptions`
116
+
117
+ | Option | Type | Default | Description |
118
+ | ----------- | --------- | ---------------- | --------------------------- |
119
+ | `siteUrl` | `string` | — | Site URL (overrides config) |
120
+ | `key` | `string` | built-in default | IndexNow API key |
121
+ | `sitemap` | `string` | — | Custom sitemap path |
122
+ | `chunkSize` | `number` | `100` | URLs per submission batch |
123
+ | `dryRun` | `boolean` | `false` | Preview without submitting |
124
+
125
+ ### `NextIndexnowResult`
126
+
127
+ ```typescript
128
+ interface NextIndexnowResult {
129
+ urlsFound: number; // Total URLs extracted from sitemap
130
+ urlsSubmitted: number; // Successfully submitted URLs
131
+ urlsFailed: number; // URLs that failed to submit
132
+ chunks: SubmissionResult[];
133
+ durationMs: number;
134
+ }
135
+ ```
136
+
137
+ ## Configuration
138
+
139
+ The tool reads your `next-sitemap.config.*` file to extract `siteUrl` and `outDir`. It supports both inline string literals and variable references:
140
+
141
+ ```javascript
142
+ // next-sitemap.config.js — variable reference style
143
+ const siteDomain = "https://example.com";
144
+
145
+ /** @type {import('next-sitemap').IConfig} */
146
+ const config = { siteUrl: siteDomain, outDir: "./public" };
147
+
148
+ module.exports = config;
149
+ ```
150
+
151
+ CLI `--site-url` option always takes precedence over the config file value.
152
+
153
+ ## License
154
+
155
+ MIT &copy; [Vijay Hardaha](https://github.com/vijayhardaha)
package/dist/cli.js ADDED
@@ -0,0 +1,747 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import logSymbols from "log-symbols";
8
+ import ora from "ora";
9
+ import xml2js from "xml2js";
10
+ //#region src/constants.ts
11
+ /**
12
+ * Constants and configuration defaults for the IndexNow CLI tool.
13
+ *
14
+ * @module constants
15
+ */
16
+ /** IndexNow API endpoint for URL submission. */
17
+ var INDEXNOW_API_URL = "https://api.indexnow.org/indexnow";
18
+ /** Sitemap output directory (relative to project root or configured outDir). */
19
+ var DEFAULT_SITEMAP_DIR = "public";
20
+ /** IndexNow key verification filename extension. */
21
+ var KEY_FILE_EXTENSION = ".txt";
22
+ /** Conventional Next.js config filenames to detect a Next.js project. */
23
+ var NEXT_CONFIG_FILES = [
24
+ "next.config.ts",
25
+ "next.config.mjs",
26
+ "next.config.cjs",
27
+ "next.config.js"
28
+ ];
29
+ /** Conventional next-sitemap config filenames. */
30
+ var NEXT_SITEMAP_CONFIG_FILES = [
31
+ "next-sitemap.config.ts",
32
+ "next-sitemap.config.mjs",
33
+ "next-sitemap.config.cjs",
34
+ "next-sitemap.config.js"
35
+ ];
36
+ /** Directories that indicate a Next.js build has been run. */
37
+ var NEXT_BUILD_DIRS = [".next"];
38
+ //#endregion
39
+ //#region src/utils.ts
40
+ /**
41
+ * Validation and helper functions for the IndexNow CLI.
42
+ *
43
+ * @module utils
44
+ */
45
+ /**
46
+ * Validate that a URL string is a valid site domain with http:// or https:// scheme.
47
+ *
48
+ * Accepts standard domains (example.com), subdomains, and localhost with ports.
49
+ *
50
+ * @param {string} url - The URL string to validate.
51
+ *
52
+ * @returns {ValidationResult} Valid result on success, or invalid with an error message.
53
+ */
54
+ function validateSiteUrl(url) {
55
+ if (!url || typeof url !== "string") return {
56
+ valid: false,
57
+ error: "Site URL is required."
58
+ };
59
+ if (!url.startsWith("http://") && !url.startsWith("https://")) return {
60
+ valid: false,
61
+ error: "Site URL must start with http:// or https://"
62
+ };
63
+ try {
64
+ const parsed = new URL(url);
65
+ /* v8 ignore next 3 */
66
+ if (!parsed.hostname) return {
67
+ valid: false,
68
+ error: "Site URL must have a valid hostname."
69
+ };
70
+ /* v8 ignore next 2 */
71
+ if (parsed.pathname !== "/" && parsed.pathname !== "" || parsed.search || parsed.hash) return {
72
+ valid: false,
73
+ error: "Site URL should be a domain root (e.g. https://example.com)."
74
+ };
75
+ } catch {
76
+ return {
77
+ valid: false,
78
+ error: `Invalid URL: "${url}". Please provide a valid URL.`
79
+ };
80
+ }
81
+ return { valid: true };
82
+ }
83
+ /**
84
+ * Check if the current working directory is a Next.js project by looking for
85
+ * a next.config.* file.
86
+ *
87
+ * @returns {ValidationResult} Valid result if a Next.js config file is found.
88
+ */
89
+ function validateNextProject() {
90
+ for (const configFile of NEXT_CONFIG_FILES) if (existsSync(resolve(process.cwd(), configFile))) return { valid: true };
91
+ return {
92
+ valid: false,
93
+ error: "No next.config.* file found. This command must be run from a Next.js project root."
94
+ };
95
+ }
96
+ /**
97
+ * Check if the `.next` build directory exists and contains files.
98
+ *
99
+ * @returns {ValidationResult} Valid result if `.next` exists and is not empty.
100
+ */
101
+ function validateDotNext() {
102
+ for (const dir of NEXT_BUILD_DIRS) {
103
+ const dotNextPath = resolve(process.cwd(), dir);
104
+ if (!existsSync(dotNextPath)) return {
105
+ valid: false,
106
+ error: `"${dir}" directory not found. Run "next build" first.`
107
+ };
108
+ try {
109
+ if (readdirSync(dotNextPath).length === 0) return {
110
+ valid: false,
111
+ error: `"${dir}" directory is empty. Run "next build" first.`
112
+ };
113
+ } catch {
114
+ /* v8 ignore next */
115
+ return {
116
+ valid: false,
117
+ error: `Cannot read "${dir}" directory.`
118
+ };
119
+ }
120
+ }
121
+ return { valid: true };
122
+ }
123
+ /**
124
+ * Check if a next-sitemap config file exists and is not empty.
125
+ *
126
+ * Searches for next-sitemap.config.{ts,mjs,cjs,js} in the current directory.
127
+ *
128
+ * @returns {ValidationResult & { filePath?: string }} Valid result with the config file path if found.
129
+ */
130
+ function validateNextSitemapConfig() {
131
+ for (const configFile of NEXT_SITEMAP_CONFIG_FILES) {
132
+ const configPath = resolve(process.cwd(), configFile);
133
+ if (existsSync(configPath)) {
134
+ if (readFileSync(configPath, "utf-8").trim().length === 0) return {
135
+ valid: false,
136
+ error: `"${configFile}" exists but is empty.`
137
+ };
138
+ return {
139
+ valid: true,
140
+ filePath: configPath
141
+ };
142
+ }
143
+ }
144
+ return {
145
+ valid: false,
146
+ error: "No next-sitemap.config.* file found. Create one to configure your sitemap."
147
+ };
148
+ }
149
+ /**
150
+ * Read the next-sitemap config file and extract the `siteUrl` and optional `outDir`.
151
+ *
152
+ * Parses the file to extract `siteUrl` from the config object, handling both
153
+ * inline string literals (`siteUrl: 'https://...'`) and variable references
154
+ * (`const siteDomain = 'https://...'; siteUrl: siteDomain`). Avoids the
155
+ * complexity of dynamic module loading for mixed CJS/ESM configs.
156
+ *
157
+ * @param {string} configPath - Absolute path to the next-sitemap config file.
158
+ *
159
+ * @returns {NextSitemapConfig | null} Parsed config, or null if siteUrl could not be extracted.
160
+ */
161
+ function readSitemapConfig(configPath) {
162
+ try {
163
+ const content = readFileSync(configPath, "utf-8");
164
+ const constMap = /* @__PURE__ */ new Map();
165
+ for (const match of content.matchAll(/const\s+(\w+)\s*=\s*['"]([^'"]+)['"]/g)) constMap.set(match[1], match[2]);
166
+ const siteUrlMatch = content.match(/siteUrl:\s*['"]([^'"]+)['"]/);
167
+ const siteUrlVarMatch = content.match(/siteUrl:\s*(\w+)/);
168
+ const siteUrl = siteUrlMatch?.[1] ?? (siteUrlVarMatch?.[1] ? constMap.get(siteUrlVarMatch[1]) : void 0);
169
+ if (!siteUrl) return null;
170
+ const outDirMatch = content.match(/outDir:\s*['"]([^'"]+)['"]/);
171
+ const outDirVarMatch = content.match(/outDir:\s*(\w+)/);
172
+ return {
173
+ siteUrl,
174
+ outDir: outDirMatch?.[1] ?? (outDirVarMatch?.[1] ? constMap.get(outDirVarMatch[1]) : void 0)
175
+ };
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+ /**
181
+ * Ensure the IndexNow verification key file exists at public/<key>.txt with the
182
+ * correct content. Creates the file and directory if they don't exist.
183
+ *
184
+ * @param {string} key - The IndexNow API key.
185
+ *
186
+ * @returns {ValidationResult} Valid result if the key file exists or was created successfully.
187
+ */
188
+ function ensureKeyFile(key) {
189
+ if (!key || typeof key !== "string") return {
190
+ valid: false,
191
+ error: "IndexNow API key is required."
192
+ };
193
+ const publicDir = resolve(process.cwd(), DEFAULT_SITEMAP_DIR);
194
+ const keyFilePath = resolve(publicDir, `${key}${KEY_FILE_EXTENSION}`);
195
+ try {
196
+ if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
197
+ if (existsSync(keyFilePath)) {
198
+ if (readFileSync(keyFilePath, "utf-8").trim() !== key) writeFileSync(keyFilePath, key, "utf-8");
199
+ } else writeFileSync(keyFilePath, key, "utf-8");
200
+ return { valid: true };
201
+ } catch (error) {
202
+ /* v8 ignore next 3 */
203
+ return {
204
+ valid: false,
205
+ error: `Failed to create key file: ${error instanceof Error ? error.message : String(error)}`
206
+ };
207
+ }
208
+ }
209
+ /**
210
+ * Resolve the sitemap file path.
211
+ *
212
+ * Uses the configured outDir (from next-sitemap.config) or falls back to
213
+ * the default `public/` directory.
214
+ *
215
+ * @param {string} [outDir] - Custom output directory from next-sitemap config.
216
+ * @param {string} [sitemapFile] - Custom sitemap filename. Defaults to sitemap-0.xml.
217
+ *
218
+ * @returns {string} Absolute path to the sitemap file.
219
+ */
220
+ function resolveSitemapPath(outDir, sitemapFile) {
221
+ const dir = outDir ?? "public";
222
+ const file = sitemapFile ?? "sitemap-0.xml";
223
+ return resolve(process.cwd(), dir, file);
224
+ }
225
+ /**
226
+ * Read and parse a sitemap XML file, extracting all `<loc>` URLs.
227
+ *
228
+ * @param {string} sitemapPath - Absolute path to the sitemap XML file.
229
+ *
230
+ * @returns {Promise<ValidationResult & { urls?: string[] }>} Valid result with URL list, or invalid with error.
231
+ */
232
+ async function readSitemap(sitemapPath) {
233
+ let content;
234
+ try {
235
+ content = await readFileSync(sitemapPath, "utf-8");
236
+ } catch {
237
+ return {
238
+ valid: false,
239
+ error: `Sitemap file not found: ${sitemapPath}`
240
+ };
241
+ }
242
+ let parsed;
243
+ try {
244
+ parsed = await xml2js.parseStringPromise(content);
245
+ } catch {
246
+ return {
247
+ valid: false,
248
+ error: "Failed to parse sitemap XML. Ensure the file is valid XML."
249
+ };
250
+ }
251
+ const urls = parsed.urlset?.url?.map((entry) => entry.loc?.[0]).filter(Boolean);
252
+ if (!urls || urls.length === 0) return {
253
+ valid: false,
254
+ error: "No URLs found in the sitemap."
255
+ };
256
+ return {
257
+ valid: true,
258
+ urls
259
+ };
260
+ }
261
+ /**
262
+ * Submit a batch of URLs to the IndexNow API.
263
+ *
264
+ * @param {string} host - The site hostname (e.g. example.com).
265
+ * @param {string} key - The IndexNow API key.
266
+ * @param {string} keyLocation - Public URL of the key verification file.
267
+ * @param {string[]} urlList - URLs to submit in this batch.
268
+ *
269
+ * @returns {Promise<SubmissionResult>} The submission result for this batch.
270
+ */
271
+ async function submitUrls(host, key, keyLocation, urlList) {
272
+ try {
273
+ const response = await fetch(INDEXNOW_API_URL, {
274
+ method: "POST",
275
+ headers: { "Content-Type": "application/json" },
276
+ body: JSON.stringify({
277
+ host,
278
+ key,
279
+ keyLocation,
280
+ urlList
281
+ })
282
+ });
283
+ if (response.ok) return {
284
+ count: urlList.length,
285
+ success: true
286
+ };
287
+ const errorText = await response.text();
288
+ return {
289
+ count: urlList.length,
290
+ success: false,
291
+ error: errorText
292
+ };
293
+ } catch (error) {
294
+ /* v8 ignore next 4 */
295
+ return {
296
+ count: urlList.length,
297
+ success: false,
298
+ error: `Network error: ${error instanceof Error ? error.message : String(error)}`
299
+ };
300
+ }
301
+ }
302
+ //#endregion
303
+ //#region src/index.ts
304
+ /**
305
+ * Core orchestration for the IndexNow CLI tool.
306
+ *
307
+ * Coordinates validation, sitemap parsing, and URL submission to the
308
+ * IndexNow API for faster search engine indexing.
309
+ *
310
+ * @module next-indexnow
311
+ */
312
+ /**
313
+ * Validate basic project structure: Next.js project and .next build directory.
314
+ *
315
+ * @throws {Error} If the project is not a Next.js project or .next is missing/empty.
316
+ */
317
+ function validateBasicEnvironment() {
318
+ const projectCheck = validateNextProject();
319
+ if (!projectCheck.valid) throw new Error(projectCheck.error);
320
+ const dotNextCheck = validateDotNext();
321
+ if (!dotNextCheck.valid) throw new Error(dotNextCheck.error);
322
+ }
323
+ /**
324
+ * Validate that a sitemap config file exists and parse its contents.
325
+ *
326
+ * @returns {NextSitemapConfig} The parsed sitemap config (siteUrl + optional outDir).
327
+ *
328
+ * @throws {Error} If no config file is found, it is empty, or siteUrl cannot be parsed.
329
+ */
330
+ function validateSitemapSetup() {
331
+ const sitemapConfigCheck = validateNextSitemapConfig();
332
+ if (!sitemapConfigCheck.valid) throw new Error(sitemapConfigCheck.error);
333
+ const parsedConfig = readSitemapConfig(sitemapConfigCheck.filePath);
334
+ if (!parsedConfig) throw new Error(`Could not parse "siteUrl" from ${sitemapConfigCheck.filePath}.`);
335
+ return parsedConfig;
336
+ }
337
+ /**
338
+ * Validate that the current directory is a Next.js project with a build
339
+ * artifact and a sitemap config file.
340
+ *
341
+ * @returns {NextSitemapConfig} The parsed sitemap config (siteUrl + optional outDir).
342
+ *
343
+ * @throws {Error} If any validation check fails.
344
+ */
345
+ function validateEnvironment() {
346
+ validateBasicEnvironment();
347
+ return validateSitemapSetup();
348
+ }
349
+ /**
350
+ * Resolve the effective siteUrl from options (takes precedence) or config file,
351
+ * validate it, and extract the hostname.
352
+ *
353
+ * @param {NextIndexnowOptions} options - CLI options.
354
+ * @param {NextSitemapConfig} parsedConfig - Parsed sitemap config.
355
+ *
356
+ * @returns {{ siteUrl: string; siteHost: string }} The resolved site URL and host.
357
+ *
358
+ * @throws {Error} If the site URL is invalid.
359
+ */
360
+ function resolveSiteConfig(options, parsedConfig) {
361
+ const siteUrl = options.siteUrl ?? parsedConfig.siteUrl;
362
+ const urlValidation = validateSiteUrl(siteUrl);
363
+ if (!urlValidation.valid) throw new Error(urlValidation.error);
364
+ return {
365
+ siteUrl,
366
+ siteHost: new URL(siteUrl).host
367
+ };
368
+ }
369
+ /**
370
+ * Resolve the IndexNow API key from options or the INDEXNOW_KEY env var,
371
+ * and ensure the verification key file exists on disk.
372
+ *
373
+ * Falls back to a hard-coded default key when neither the --key option nor
374
+ * the INDEXNOW_KEY environment variable is provided.
375
+ *
376
+ * @param {NextIndexnowOptions} options - CLI options.
377
+ * @param {string} siteUrl - The resolved site URL (for keyLocation).
378
+ *
379
+ * @returns {{ key: string; keyLocation: string }} The resolved key and its public URL.
380
+ *
381
+ * @throws {Error} If the key file cannot be created.
382
+ */
383
+ function resolveKeyValue(options, siteUrl) {
384
+ const key = options.key ?? process.env.INDEXNOW_KEY ?? "91c80f732f4e4e5b80b4c02a7e8c9e9c";
385
+ const keyFileCheck = ensureKeyFile(key);
386
+ if (!keyFileCheck.valid) throw new Error(keyFileCheck.error);
387
+ return {
388
+ key,
389
+ keyLocation: `${siteUrl}/${key}.txt`
390
+ };
391
+ }
392
+ /**
393
+ * Resolve the sitemap path (from options or default) and read all URLs.
394
+ *
395
+ * @param {NextIndexnowOptions} options - CLI options.
396
+ * @param {NextSitemapConfig} parsedConfig - Parsed sitemap config (for outDir).
397
+ *
398
+ * @returns {Promise<string[]>} The list of URLs found in the sitemap.
399
+ *
400
+ * @throws {Error} If the sitemap cannot be read or contains no URLs.
401
+ */
402
+ async function loadSitemapUrls(options, parsedConfig) {
403
+ const sitemapResult = await readSitemap(options.sitemap ?? resolveSitemapPath(parsedConfig.outDir));
404
+ if (!sitemapResult.valid) throw new Error(sitemapResult.error);
405
+ return sitemapResult.urls;
406
+ }
407
+ /**
408
+ * Submit all URLs to the IndexNow API in chunks and aggregate the results.
409
+ *
410
+ * @param {string[]} urls - All URLs to submit.
411
+ * @param {string} siteHost - The site hostname.
412
+ * @param {string} key - The IndexNow API key.
413
+ * @param {string} keyLocation - Public URL of the key verification file.
414
+ * @param {number} chunkSize - Maximum URLs per submission batch.
415
+ * @param {IndexnowProgress} [onProgress] - Callback invoked before each batch submission.
416
+ *
417
+ * @returns {Promise<Pick<NextIndexnowResult, 'urlsSubmitted' | 'urlsFailed' | 'chunks'>>} Aggregate submission counts and per-chunk details.
418
+ */
419
+ async function submitAllUrls(urls, siteHost, key, keyLocation, chunkSize, onProgress) {
420
+ const chunks = [];
421
+ const totalChunks = Math.ceil(urls.length / chunkSize);
422
+ for (let i = 0; i < urls.length; i += chunkSize) {
423
+ const batchNum = Math.floor(i / chunkSize) + 1;
424
+ const chunk = urls.slice(i, i + chunkSize);
425
+ onProgress?.({
426
+ batch: batchNum,
427
+ totalBatches: totalChunks,
428
+ urlCount: chunk.length
429
+ });
430
+ const result = await submitUrls(siteHost, key, keyLocation, chunk);
431
+ chunks.push(result);
432
+ }
433
+ return {
434
+ urlsSubmitted: chunks.filter((c) => c.success).reduce((sum, c) => sum + c.count, 0),
435
+ urlsFailed: chunks.filter((c) => !c.success).reduce((sum, c) => sum + c.count, 0),
436
+ chunks
437
+ };
438
+ }
439
+ /**
440
+ * Run the IndexNow submission process.
441
+ *
442
+ * Validates the environment (Next.js project, .next dir, sitemap config),
443
+ * reads the sitemap, and submits all URLs to the IndexNow API in chunks.
444
+ *
445
+ * @param {NextIndexnowOptions} options - CLI options (siteUrl, key, sitemap, chunkSize, dryRun, onProgress).
446
+ *
447
+ * @returns {Promise<NextIndexnowResult>} Aggregate result with per-chunk details.
448
+ */
449
+ async function run(options = {}) {
450
+ const startTime = Date.now();
451
+ const chunkSize = options.chunkSize ?? 100;
452
+ const parsedConfig = validateEnvironment();
453
+ const { siteUrl, siteHost } = resolveSiteConfig(options, parsedConfig);
454
+ const { key, keyLocation } = resolveKeyValue(options, siteUrl);
455
+ const urls = await loadSitemapUrls(options, parsedConfig);
456
+ if (options.dryRun) return {
457
+ urlsFound: urls.length,
458
+ urlsSubmitted: 0,
459
+ urlsFailed: 0,
460
+ chunks: [],
461
+ durationMs: Date.now() - startTime
462
+ };
463
+ const { urlsSubmitted, urlsFailed, chunks } = await submitAllUrls(urls, siteHost, key, keyLocation, chunkSize, options.onProgress);
464
+ return {
465
+ urlsFound: urls.length,
466
+ urlsSubmitted,
467
+ urlsFailed,
468
+ chunks,
469
+ durationMs: Date.now() - startTime
470
+ };
471
+ }
472
+ //#endregion
473
+ //#region src/bin/cli.ts
474
+ /**
475
+ * next-indexnow — CLI entry point.
476
+ *
477
+ * Parses command-line arguments with commander, validates the environment,
478
+ * reads the Next.js sitemap, and submits URLs to the IndexNow API.
479
+ *
480
+ * @module cli
481
+ *
482
+ * Usage:
483
+ * next-indexnow
484
+ * next-indexnow --site-url https://example.com
485
+ * next-indexnow --key my-api-key
486
+ * next-indexnow --sitemap ./public/sitemap-0.xml
487
+ * next-indexnow --dry-run
488
+ * next-indexnow --help
489
+ */
490
+ var BANNER = `
491
+ ${chalk.white("██╗███╗ ██╗██████╗ ███████╗██╗ ██╗███╗ ██╗ ██████╗ ██╗ ██╗")}
492
+ ${chalk.white("██║████╗ ██║██╔══██╗██╔════╝╚██╗██╔╝████╗ ██║██╔═══██╗██║ ██║")}
493
+ ${chalk.white("██║██╔██╗ ██║██║ ██║█████╗ ╚███╔╝ ██╔██╗ ██║██║ ██║██║ █╗ ██║")}
494
+ ${chalk.white("██║██║╚██╗██║██║ ██║██╔══╝ ██╔██╗ ██║╚██╗██║██║ ██║██║███╗██║")}
495
+ ${chalk.white("██║██║ ╚████║██████╔╝███████╗██╔╝ ██╗██║ ╚████║╚██████╔╝╚███╔███╔╝")}
496
+ ${chalk.white("╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══╝╚══╝")}`;
497
+ var SEPARATOR = chalk.dim("=".repeat(69));
498
+ /**
499
+ * Print a checkmark or error icon with a status label and optional detail.
500
+ *
501
+ * @param {boolean} valid - Whether the check passed.
502
+ * @param {string} label - The status label text.
503
+ * @param {string} [detail] - Optional detail text shown after a colon.
504
+ */
505
+ function checkMark(valid, label, detail) {
506
+ const icon = valid ? logSymbols.success : logSymbols.error;
507
+ const msg = detail ? `${label}: ${detail}` : label;
508
+ console.log(`${icon} ${msg}`);
509
+ }
510
+ /**
511
+ * Print the summary footer message after a submission run.
512
+ *
513
+ * @param {NextIndexnowResult} result - The result from the IndexNow submission run.
514
+ * @param {boolean} dryRun - Whether this was a dry-run preview.
515
+ */
516
+ function printFooter(result, dryRun) {
517
+ console.log("");
518
+ if (dryRun) console.log(chalk.green(`Dry-run complete. ${result.urlsFound} urls would be submitted. 🚀`));
519
+ else if (result.urlsFailed === 0) console.log(chalk.green(`All ${result.urlsSubmitted} urls submitted to IndexNow successfully! 🥳`));
520
+ else console.log(chalk.yellow(`${result.urlsSubmitted}/${result.urlsFound} urls submitted successfully (${result.urlsFailed} failed).`));
521
+ }
522
+ /**
523
+ * Print detailed failure information for failed submission batches.
524
+ *
525
+ * @param {NextIndexnowResult} result - The result from the IndexNow submission run.
526
+ */
527
+ function printFailures(result) {
528
+ if (result.urlsFailed === 0) return;
529
+ console.error(chalk.red.bold("Failed submissions:"));
530
+ for (const chunk of result.chunks) if (!chunk.success) console.error(chalk.red(` ${logSymbols.error} ${chunk.error}`));
531
+ }
532
+ var spinner = null;
533
+ process.on("SIGINT", () => {
534
+ if (spinner?.isSpinning) spinner.stop();
535
+ console.log("");
536
+ console.log(chalk.yellow("Process aborted by user."));
537
+ process.exit(130);
538
+ });
539
+ var __filename = fileURLToPath(import.meta.url);
540
+ var pkgPathSrc = resolve(__filename, "..", "..", "..", "package.json");
541
+ var pkgPathDist = resolve(__filename, "..", "..", "package.json");
542
+ var pkgPath = existsSync(pkgPathDist) ? pkgPathDist : pkgPathSrc;
543
+ var pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
544
+ var program = new Command();
545
+ program.name("next-indexnow").description("Submit Next.js sitemap URLs to the IndexNow API for faster search engine indexing").version(pkg.version).option("--site-url <url>", "The site URL (e.g. https://example.com). Overrides next-sitemap.config value.").option("--key <key>", "IndexNow API key. Falls back to INDEXNOW_KEY environment variable or a built-in default key.").option("--sitemap <path>", "Path to the sitemap XML file").option("--chunk-size <number>", "URLs per submission batch", (v) => Number.parseInt(v, 10), 100).option("-d, --dry-run", "Preview URLs without submitting to the IndexNow API").addHelpText("after", `
546
+ Examples:
547
+ $ next-indexnow Submit URLs using settings from next-sitemap.config
548
+ $ next-indexnow --site-url https://example.com Override the site URL
549
+ $ next-indexnow --key my-api-key Provide IndexNow API key
550
+ $ next-indexnow --sitemap ./public/sitemap.xml Use a custom sitemap path
551
+ $ next-indexnow --dry-run Preview URLs without submitting
552
+ $ next-indexnow --help Show this help message
553
+ `).parse(process.argv);
554
+ /**
555
+ * Log submission results as checkmark-style lines.
556
+ *
557
+ * @param {NextIndexnowResult} result - The result from the IndexNow submission run.
558
+ */
559
+ function displayResults(result) {
560
+ const duration = (result.durationMs / 1e3).toFixed(2);
561
+ const items = [
562
+ [
563
+ "URLs found",
564
+ String(result.urlsFound),
565
+ result.urlsFound > 0 ? "blue" : "dim"
566
+ ],
567
+ [
568
+ "URLs submitted",
569
+ String(result.urlsSubmitted),
570
+ result.urlsSubmitted > 0 ? "green" : "dim"
571
+ ],
572
+ [
573
+ "URLs failed",
574
+ String(result.urlsFailed),
575
+ result.urlsFailed > 0 ? "red" : "dim"
576
+ ],
577
+ [
578
+ "Duration",
579
+ `${duration}s`,
580
+ result.durationMs > 0 ? "yellow" : "dim"
581
+ ]
582
+ ];
583
+ console.log("");
584
+ for (const [label, value, color] of items) {
585
+ let coloredValue;
586
+ switch (color) {
587
+ case "red":
588
+ coloredValue = chalk.red(value);
589
+ break;
590
+ case "dim":
591
+ coloredValue = chalk.dim(value);
592
+ break;
593
+ case "green":
594
+ coloredValue = chalk.green(value);
595
+ break;
596
+ case "blue":
597
+ coloredValue = chalk.blue(value);
598
+ break;
599
+ case "yellow":
600
+ coloredValue = chalk.yellow(value);
601
+ break;
602
+ default:
603
+ coloredValue = value;
604
+ break;
605
+ }
606
+ console.log(`${logSymbols.success} ${chalk.bold(label)}: ${coloredValue}`);
607
+ }
608
+ }
609
+ /**
610
+ * Run validation checks and display checkmarks, returning parsed state.
611
+ *
612
+ * @param {object} opts - CLI options for site URL and API key.
613
+ * @param {string} [opts.siteUrl] - Override site URL from --site-url option.
614
+ * @param {string} [opts.key] - Override API key from --key option.
615
+ *
616
+ * @returns {{ parsedConfig: NextSitemapConfig; siteUrl: string; siteHost: string; key: string; keyLocation: string }} Resolved configuration values.
617
+ */
618
+ function runValidationChecks(opts) {
619
+ const projectCheck = validateNextProject();
620
+ if (!projectCheck.valid) {
621
+ checkMark(false, "Next.js project", projectCheck.error);
622
+ console.log(chalk.red(`\n ${projectCheck.error}`));
623
+ process.exit(1);
624
+ }
625
+ checkMark(true, "Next.js config found");
626
+ const dotNextCheck = validateDotNext();
627
+ if (!dotNextCheck.valid) {
628
+ checkMark(false, "Build directory", dotNextCheck.error);
629
+ console.log(chalk.red(`\n ${dotNextCheck.error}`));
630
+ process.exit(1);
631
+ }
632
+ checkMark(true, "Build directory exists (.next)");
633
+ const sitemapConfigCheck = validateNextSitemapConfig();
634
+ if (!sitemapConfigCheck.valid) {
635
+ checkMark(false, "Sitemap config", sitemapConfigCheck.error);
636
+ console.log(chalk.red(`\n ${sitemapConfigCheck.error}`));
637
+ process.exit(1);
638
+ }
639
+ checkMark(true, "Sitemap config found", sitemapConfigCheck.filePath);
640
+ const parsedConfig = readSitemapConfig(sitemapConfigCheck.filePath);
641
+ if (!parsedConfig) {
642
+ checkMark(false, "Site URL", "Could not parse siteUrl from config");
643
+ console.log(chalk.red(`\n Could not parse "siteUrl" from ${sitemapConfigCheck.filePath}.`));
644
+ process.exit(1);
645
+ }
646
+ const siteUrl = opts.siteUrl ?? parsedConfig.siteUrl;
647
+ const urlValidation = validateSiteUrl(siteUrl);
648
+ if (!urlValidation.valid) {
649
+ checkMark(false, "Site URL", urlValidation.error);
650
+ console.log(chalk.red(`\n ${urlValidation.error}`));
651
+ process.exit(1);
652
+ }
653
+ const siteHost = new URL(siteUrl).host;
654
+ checkMark(true, "Site URL resolved", siteHost);
655
+ const key = opts.key ?? process.env.INDEXNOW_KEY ?? "91c80f732f4e4e5b80b4c02a7e8c9e9c";
656
+ if (opts.key) checkMark(true, "API key provided", "via --key option");
657
+ else if (process.env.INDEXNOW_KEY) checkMark(true, "API key resolved", "via INDEXNOW_KEY env");
658
+ else checkMark(true, "API key resolved", "using built-in default key");
659
+ const keyFileCheck = ensureKeyFile(key);
660
+ if (!keyFileCheck.valid) {
661
+ checkMark(false, "Key file", keyFileCheck.error);
662
+ console.log(chalk.red(`\n ${keyFileCheck.error}`));
663
+ process.exit(1);
664
+ }
665
+ checkMark(true, "Key verification file", existsSync(resolve(process.cwd(), "public", `${key}.txt`)) ? `${key}.txt exists` : `${key}.txt created`);
666
+ return {
667
+ parsedConfig,
668
+ siteUrl,
669
+ siteHost,
670
+ key,
671
+ keyLocation: `${siteUrl}/${key}.txt`
672
+ };
673
+ }
674
+ /**
675
+ * Read sitemap and display checkmark with URL count.
676
+ *
677
+ * @param {object} opts - CLI options for sitemap path.
678
+ * @param {string} [opts.sitemap] - Custom sitemap path from --sitemap option.
679
+ * @param {NextSitemapConfig} parsedConfig - Parsed sitemap config for default outDir.
680
+ *
681
+ * @returns {Promise<string[]>} The list of URLs extracted from the sitemap.
682
+ */
683
+ async function loadSitemapCheck(opts, parsedConfig) {
684
+ const sitemapResult = await readSitemap(opts.sitemap ?? resolveSitemapPath(parsedConfig.outDir));
685
+ if (!sitemapResult.valid) {
686
+ checkMark(false, "Sitemap", sitemapResult.error);
687
+ console.log(chalk.red(`\n ${sitemapResult.error}`));
688
+ process.exit(1);
689
+ }
690
+ const urlCount = sitemapResult.urls.length;
691
+ checkMark(true, "Sitemap loaded", `${urlCount} URLs found`);
692
+ return sitemapResult.urls;
693
+ }
694
+ /**
695
+ * Main CLI entry point.
696
+ *
697
+ * Displays the ASCII banner, runs validation checks with checkmarks,
698
+ * submits URLs to the IndexNow API, and shows results with a footer.
699
+ */
700
+ async function main() {
701
+ const opts = program.optsWithGlobals();
702
+ console.log(BANNER);
703
+ console.log("");
704
+ console.log(SEPARATOR);
705
+ console.log(chalk.yellow(`next-indexnow: v${pkg.version}`));
706
+ console.log(SEPARATOR);
707
+ console.log("");
708
+ const { parsedConfig } = runValidationChecks({
709
+ siteUrl: opts.siteUrl,
710
+ key: opts.key
711
+ });
712
+ await loadSitemapCheck({ sitemap: opts.sitemap }, parsedConfig);
713
+ console.log("");
714
+ spinner = ora({
715
+ color: "cyan",
716
+ discardStdin: false
717
+ });
718
+ const result = await run({
719
+ siteUrl: opts.siteUrl,
720
+ key: opts.key,
721
+ sitemap: opts.sitemap,
722
+ chunkSize: opts.chunkSize,
723
+ dryRun: opts.dryRun ?? false,
724
+ onProgress: ({ batch, totalBatches, urlCount }) => {
725
+ if (spinner) {
726
+ spinner.text = `Submitting batch ${batch}/${totalBatches} (${urlCount} URLs)`;
727
+ if (!spinner.isSpinning) spinner.start();
728
+ }
729
+ }
730
+ });
731
+ if (spinner?.isSpinning) spinner.stop();
732
+ console.log(SEPARATOR);
733
+ console.log(`${logSymbols.success} Submission Completed 🎉`);
734
+ console.log(SEPARATOR);
735
+ displayResults(result);
736
+ printFooter(result, opts.dryRun);
737
+ printFailures(result);
738
+ if (result.urlsFailed > 0) process.exit(1);
739
+ }
740
+ main().catch((error) => {
741
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : String(error));
742
+ process.exit(1);
743
+ });
744
+ //#endregion
745
+ export { main };
746
+
747
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/constants.ts","../src/utils.ts","../src/index.ts","../src/bin/cli.ts"],"sourcesContent":["/**\n * Constants and configuration defaults for the IndexNow CLI tool.\n *\n * @module constants\n */\n\n/** IndexNow API endpoint for URL submission. */\nexport const INDEXNOW_API_URL = 'https://api.indexnow.org/indexnow';\n\n/** Number of URLs to submit per API request. */\nexport const CHUNK_SIZE = 100;\n\n/** Default sitemap filename (generated by next-sitemap). */\nexport const DEFAULT_SITEMAP_FILE = 'sitemap-0.xml';\n\n/** Sitemap output directory (relative to project root or configured outDir). */\nexport const DEFAULT_SITEMAP_DIR = 'public';\n\n/** IndexNow key verification filename extension. */\nexport const KEY_FILE_EXTENSION = '.txt';\n\n/** Default IndexNow API key — used when neither --key option nor INDEXNOW_KEY env var is provided. */\nexport const DEFAULT_INDEXNOW_KEY = '91c80f732f4e4e5b80b4c02a7e8c9e9c';\n\n/** Conventional Next.js config filenames to detect a Next.js project. */\nexport const NEXT_CONFIG_FILES = ['next.config.ts', 'next.config.mjs', 'next.config.cjs', 'next.config.js'];\n\n/** Conventional next-sitemap config filenames. */\nexport const NEXT_SITEMAP_CONFIG_FILES = [\n 'next-sitemap.config.ts',\n 'next-sitemap.config.mjs',\n 'next-sitemap.config.cjs',\n 'next-sitemap.config.js',\n];\n\n/** Directories that indicate a Next.js build has been run. */\nexport const NEXT_BUILD_DIRS = ['.next'];\n","/**\n * Validation and helper functions for the IndexNow CLI.\n *\n * @module utils\n */\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport xml2js from 'xml2js';\n\nimport {\n INDEXNOW_API_URL,\n NEXT_CONFIG_FILES,\n NEXT_SITEMAP_CONFIG_FILES,\n NEXT_BUILD_DIRS,\n KEY_FILE_EXTENSION,\n DEFAULT_SITEMAP_DIR,\n DEFAULT_SITEMAP_FILE,\n} from './constants.ts';\nimport type { NextSitemapConfig, ValidationResult, SubmissionResult } from './types.ts';\n\n/**\n * Validate that a URL string is a valid site domain with http:// or https:// scheme.\n *\n * Accepts standard domains (example.com), subdomains, and localhost with ports.\n *\n * @param {string} url - The URL string to validate.\n *\n * @returns {ValidationResult} Valid result on success, or invalid with an error message.\n */\nexport function validateSiteUrl(url: string): ValidationResult {\n if (!url || typeof url !== 'string') {\n return { valid: false, error: 'Site URL is required.' };\n }\n\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\n return { valid: false, error: 'Site URL must start with http:// or https://' };\n }\n\n try {\n const parsed = new URL(url);\n\n /* v8 ignore next 3 */\n if (!parsed.hostname) {\n return { valid: false, error: 'Site URL must have a valid hostname.' };\n }\n\n // Reject URLs with path, query, or fragment beyond a single trailing slash\n /* v8 ignore next 2 */\n if ((parsed.pathname !== '/' && parsed.pathname !== '') || parsed.search || parsed.hash) {\n return { valid: false, error: 'Site URL should be a domain root (e.g. https://example.com).' };\n }\n } catch {\n return { valid: false, error: `Invalid URL: \"${url}\". Please provide a valid URL.` };\n }\n\n return { valid: true };\n}\n\n/**\n * Check if the current working directory is a Next.js project by looking for\n * a next.config.* file.\n *\n * @returns {ValidationResult} Valid result if a Next.js config file is found.\n */\nexport function validateNextProject(): ValidationResult {\n for (const configFile of NEXT_CONFIG_FILES) {\n if (existsSync(resolve(process.cwd(), configFile))) {\n return { valid: true };\n }\n }\n\n return { valid: false, error: 'No next.config.* file found. This command must be run from a Next.js project root.' };\n}\n\n/**\n * Check if the `.next` build directory exists and contains files.\n *\n * @returns {ValidationResult} Valid result if `.next` exists and is not empty.\n */\nexport function validateDotNext(): ValidationResult {\n for (const dir of NEXT_BUILD_DIRS) {\n const dotNextPath = resolve(process.cwd(), dir);\n\n if (!existsSync(dotNextPath)) {\n return { valid: false, error: `\"${dir}\" directory not found. Run \"next build\" first.` };\n }\n\n try {\n const entries = readdirSync(dotNextPath);\n if (entries.length === 0) {\n return { valid: false, error: `\"${dir}\" directory is empty. Run \"next build\" first.` };\n }\n } catch {\n /* v8 ignore next */\n return { valid: false, error: `Cannot read \"${dir}\" directory.` };\n }\n }\n\n return { valid: true };\n}\n\n/**\n * Check if a next-sitemap config file exists and is not empty.\n *\n * Searches for next-sitemap.config.{ts,mjs,cjs,js} in the current directory.\n *\n * @returns {ValidationResult & { filePath?: string }} Valid result with the config file path if found.\n */\nexport function validateNextSitemapConfig(): ValidationResult & { filePath?: string } {\n for (const configFile of NEXT_SITEMAP_CONFIG_FILES) {\n const configPath = resolve(process.cwd(), configFile);\n\n if (existsSync(configPath)) {\n const content = readFileSync(configPath, 'utf-8').trim();\n\n if (content.length === 0) {\n return { valid: false, error: `\"${configFile}\" exists but is empty.` };\n }\n\n return { valid: true, filePath: configPath };\n }\n }\n\n return { valid: false, error: 'No next-sitemap.config.* file found. Create one to configure your sitemap.' };\n}\n\n/**\n * Read the next-sitemap config file and extract the `siteUrl` and optional `outDir`.\n *\n * Parses the file to extract `siteUrl` from the config object, handling both\n * inline string literals (`siteUrl: 'https://...'`) and variable references\n * (`const siteDomain = 'https://...'; siteUrl: siteDomain`). Avoids the\n * complexity of dynamic module loading for mixed CJS/ESM configs.\n *\n * @param {string} configPath - Absolute path to the next-sitemap config file.\n *\n * @returns {NextSitemapConfig | null} Parsed config, or null if siteUrl could not be extracted.\n */\nexport function readSitemapConfig(configPath: string): NextSitemapConfig | null {\n try {\n const content = readFileSync(configPath, 'utf-8');\n\n // Build a map of const variable names to their string literal values\n const constMap = new Map<string, string>();\n for (const match of content.matchAll(/const\\s+(\\w+)\\s*=\\s*['\"]([^'\"]+)['\"]/g)) {\n constMap.set(match[1]!, match[2]!);\n }\n\n // Try inline string literal first, then variable reference lookup\n const siteUrlMatch = content.match(/siteUrl:\\s*['\"]([^'\"]+)['\"]/);\n const siteUrlVarMatch = content.match(/siteUrl:\\s*(\\w+)/);\n\n const siteUrl = siteUrlMatch?.[1] ?? (siteUrlVarMatch?.[1] ? constMap.get(siteUrlVarMatch[1]) : undefined);\n\n if (!siteUrl) {\n return null;\n }\n\n // Extract optional outDir value (inline string literal or variable)\n const outDirMatch = content.match(/outDir:\\s*['\"]([^'\"]+)['\"]/);\n const outDirVarMatch = content.match(/outDir:\\s*(\\w+)/);\n const outDir = outDirMatch?.[1] ?? (outDirVarMatch?.[1] ? constMap.get(outDirVarMatch[1]) : undefined);\n\n return { siteUrl, outDir };\n } catch {\n return null;\n }\n}\n\n/**\n * Ensure the IndexNow verification key file exists at public/<key>.txt with the\n * correct content. Creates the file and directory if they don't exist.\n *\n * @param {string} key - The IndexNow API key.\n *\n * @returns {ValidationResult} Valid result if the key file exists or was created successfully.\n */\nexport function ensureKeyFile(key: string): ValidationResult {\n if (!key || typeof key !== 'string') {\n return { valid: false, error: 'IndexNow API key is required.' };\n }\n\n const publicDir = resolve(process.cwd(), DEFAULT_SITEMAP_DIR);\n const keyFilePath = resolve(publicDir, `${key}${KEY_FILE_EXTENSION}`);\n\n try {\n if (!existsSync(publicDir)) {\n mkdirSync(publicDir, { recursive: true });\n }\n\n if (existsSync(keyFilePath)) {\n const existingContent = readFileSync(keyFilePath, 'utf-8').trim();\n\n if (existingContent !== key) {\n writeFileSync(keyFilePath, key, 'utf-8');\n }\n } else {\n writeFileSync(keyFilePath, key, 'utf-8');\n }\n\n return { valid: true };\n } catch (error) {\n /* v8 ignore next 3 */\n return {\n valid: false,\n error: `Failed to create key file: ${error instanceof Error ? error.message : String(error)}`,\n };\n }\n}\n\n/**\n * Resolve the sitemap file path.\n *\n * Uses the configured outDir (from next-sitemap.config) or falls back to\n * the default `public/` directory.\n *\n * @param {string} [outDir] - Custom output directory from next-sitemap config.\n * @param {string} [sitemapFile] - Custom sitemap filename. Defaults to sitemap-0.xml.\n *\n * @returns {string} Absolute path to the sitemap file.\n */\nexport function resolveSitemapPath(outDir?: string, sitemapFile?: string): string {\n const dir = outDir ?? DEFAULT_SITEMAP_DIR;\n const file = sitemapFile ?? DEFAULT_SITEMAP_FILE;\n return resolve(process.cwd(), dir, file);\n}\n\n/**\n * Read and parse a sitemap XML file, extracting all `<loc>` URLs.\n *\n * @param {string} sitemapPath - Absolute path to the sitemap XML file.\n *\n * @returns {Promise<ValidationResult & { urls?: string[] }>} Valid result with URL list, or invalid with error.\n */\nexport async function readSitemap(sitemapPath: string): Promise<ValidationResult & { urls?: string[] }> {\n let content: string;\n\n try {\n content = await readFileSync(sitemapPath, 'utf-8');\n } catch {\n return { valid: false, error: `Sitemap file not found: ${sitemapPath}` };\n }\n\n let parsed: { urlset?: { url?: Array<{ loc?: string[] }> } };\n\n try {\n parsed = await xml2js.parseStringPromise(content);\n } catch {\n return { valid: false, error: 'Failed to parse sitemap XML. Ensure the file is valid XML.' };\n }\n\n const urls = parsed.urlset?.url?.map((entry) => entry.loc?.[0]).filter(Boolean) as string[] | undefined;\n\n if (!urls || urls.length === 0) {\n return { valid: false, error: 'No URLs found in the sitemap.' };\n }\n\n return { valid: true, urls };\n}\n\n/**\n * Submit a batch of URLs to the IndexNow API.\n *\n * @param {string} host - The site hostname (e.g. example.com).\n * @param {string} key - The IndexNow API key.\n * @param {string} keyLocation - Public URL of the key verification file.\n * @param {string[]} urlList - URLs to submit in this batch.\n *\n * @returns {Promise<SubmissionResult>} The submission result for this batch.\n */\nexport async function submitUrls(\n host: string,\n key: string,\n keyLocation: string,\n urlList: string[]\n): Promise<SubmissionResult> {\n try {\n const response = await fetch(INDEXNOW_API_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ host, key, keyLocation, urlList }),\n });\n\n if (response.ok) {\n return { count: urlList.length, success: true };\n }\n\n const errorText = await response.text();\n return { count: urlList.length, success: false, error: errorText };\n } catch (error) {\n /* v8 ignore next 4 */\n return {\n count: urlList.length,\n success: false,\n error: `Network error: ${error instanceof Error ? error.message : String(error)}`,\n };\n }\n}\n","/**\n * Core orchestration for the IndexNow CLI tool.\n *\n * Coordinates validation, sitemap parsing, and URL submission to the\n * IndexNow API for faster search engine indexing.\n *\n * @module next-indexnow\n */\n\nimport { CHUNK_SIZE, DEFAULT_INDEXNOW_KEY } from './constants.ts';\nimport type {\n NextIndexnowOptions,\n NextIndexnowResult,\n NextSitemapConfig,\n SubmissionResult,\n IndexnowProgress,\n} from './types.ts';\nimport {\n validateSiteUrl,\n validateNextProject,\n validateDotNext,\n validateNextSitemapConfig,\n readSitemapConfig,\n ensureKeyFile,\n resolveSitemapPath,\n readSitemap,\n submitUrls,\n} from './utils.ts';\n\n/**\n * Validate basic project structure: Next.js project and .next build directory.\n *\n * @throws {Error} If the project is not a Next.js project or .next is missing/empty.\n */\nfunction validateBasicEnvironment(): void {\n const projectCheck = validateNextProject();\n if (!projectCheck.valid) {\n throw new Error(projectCheck.error);\n }\n\n const dotNextCheck = validateDotNext();\n if (!dotNextCheck.valid) {\n throw new Error(dotNextCheck.error);\n }\n}\n\n/**\n * Validate that a sitemap config file exists and parse its contents.\n *\n * @returns {NextSitemapConfig} The parsed sitemap config (siteUrl + optional outDir).\n *\n * @throws {Error} If no config file is found, it is empty, or siteUrl cannot be parsed.\n */\nfunction validateSitemapSetup(): NextSitemapConfig {\n const sitemapConfigCheck = validateNextSitemapConfig();\n if (!sitemapConfigCheck.valid) {\n throw new Error(sitemapConfigCheck.error!);\n }\n\n const parsedConfig = readSitemapConfig(sitemapConfigCheck.filePath!);\n if (!parsedConfig) {\n throw new Error(`Could not parse \"siteUrl\" from ${sitemapConfigCheck.filePath}.`);\n }\n\n return parsedConfig;\n}\n\n/**\n * Validate that the current directory is a Next.js project with a build\n * artifact and a sitemap config file.\n *\n * @returns {NextSitemapConfig} The parsed sitemap config (siteUrl + optional outDir).\n *\n * @throws {Error} If any validation check fails.\n */\nfunction validateEnvironment(): NextSitemapConfig {\n validateBasicEnvironment();\n return validateSitemapSetup();\n}\n\n/**\n * Resolve the effective siteUrl from options (takes precedence) or config file,\n * validate it, and extract the hostname.\n *\n * @param {NextIndexnowOptions} options - CLI options.\n * @param {NextSitemapConfig} parsedConfig - Parsed sitemap config.\n *\n * @returns {{ siteUrl: string; siteHost: string }} The resolved site URL and host.\n *\n * @throws {Error} If the site URL is invalid.\n */\nfunction resolveSiteConfig(\n options: NextIndexnowOptions,\n parsedConfig: NextSitemapConfig\n): { siteUrl: string; siteHost: string } {\n const siteUrl = options.siteUrl ?? parsedConfig.siteUrl;\n\n const urlValidation = validateSiteUrl(siteUrl);\n if (!urlValidation.valid) {\n throw new Error(urlValidation.error);\n }\n\n return { siteUrl, siteHost: new URL(siteUrl).host };\n}\n\n/**\n * Resolve the IndexNow API key from options or the INDEXNOW_KEY env var,\n * and ensure the verification key file exists on disk.\n *\n * Falls back to a hard-coded default key when neither the --key option nor\n * the INDEXNOW_KEY environment variable is provided.\n *\n * @param {NextIndexnowOptions} options - CLI options.\n * @param {string} siteUrl - The resolved site URL (for keyLocation).\n *\n * @returns {{ key: string; keyLocation: string }} The resolved key and its public URL.\n *\n * @throws {Error} If the key file cannot be created.\n */\nfunction resolveKeyValue(options: NextIndexnowOptions, siteUrl: string): { key: string; keyLocation: string } {\n const key = options.key ?? process.env.INDEXNOW_KEY ?? DEFAULT_INDEXNOW_KEY;\n\n const keyFileCheck = ensureKeyFile(key);\n if (!keyFileCheck.valid) {\n throw new Error(keyFileCheck.error);\n }\n\n return { key, keyLocation: `${siteUrl}/${key}.txt` };\n}\n\n/**\n * Resolve the sitemap path (from options or default) and read all URLs.\n *\n * @param {NextIndexnowOptions} options - CLI options.\n * @param {NextSitemapConfig} parsedConfig - Parsed sitemap config (for outDir).\n *\n * @returns {Promise<string[]>} The list of URLs found in the sitemap.\n *\n * @throws {Error} If the sitemap cannot be read or contains no URLs.\n */\nasync function loadSitemapUrls(options: NextIndexnowOptions, parsedConfig: NextSitemapConfig): Promise<string[]> {\n const sitemapPath = options.sitemap ?? resolveSitemapPath(parsedConfig.outDir);\n\n const sitemapResult = await readSitemap(sitemapPath);\n if (!sitemapResult.valid) {\n throw new Error(sitemapResult.error!);\n }\n\n return sitemapResult.urls!;\n}\n\n/**\n * Submit all URLs to the IndexNow API in chunks and aggregate the results.\n *\n * @param {string[]} urls - All URLs to submit.\n * @param {string} siteHost - The site hostname.\n * @param {string} key - The IndexNow API key.\n * @param {string} keyLocation - Public URL of the key verification file.\n * @param {number} chunkSize - Maximum URLs per submission batch.\n * @param {IndexnowProgress} [onProgress] - Callback invoked before each batch submission.\n *\n * @returns {Promise<Pick<NextIndexnowResult, 'urlsSubmitted' | 'urlsFailed' | 'chunks'>>} Aggregate submission counts and per-chunk details.\n */\nasync function submitAllUrls(\n urls: string[],\n siteHost: string,\n key: string,\n keyLocation: string,\n chunkSize: number,\n onProgress?: IndexnowProgress\n): Promise<Pick<NextIndexnowResult, 'urlsSubmitted' | 'urlsFailed' | 'chunks'>> {\n const chunks: SubmissionResult[] = [];\n const totalChunks = Math.ceil(urls.length / chunkSize);\n\n for (let i = 0; i < urls.length; i += chunkSize) {\n const batchNum = Math.floor(i / chunkSize) + 1;\n const chunk = urls.slice(i, i + chunkSize);\n\n onProgress?.({ batch: batchNum, totalBatches: totalChunks, urlCount: chunk.length });\n\n const result = await submitUrls(siteHost, key, keyLocation, chunk);\n chunks.push(result);\n }\n\n const urlsSubmitted = chunks.filter((c) => c.success).reduce((sum, c) => sum + c.count, 0);\n const urlsFailed = chunks.filter((c) => !c.success).reduce((sum, c) => sum + c.count, 0);\n\n return { urlsSubmitted, urlsFailed, chunks };\n}\n\n/**\n * Run the IndexNow submission process.\n *\n * Validates the environment (Next.js project, .next dir, sitemap config),\n * reads the sitemap, and submits all URLs to the IndexNow API in chunks.\n *\n * @param {NextIndexnowOptions} options - CLI options (siteUrl, key, sitemap, chunkSize, dryRun, onProgress).\n *\n * @returns {Promise<NextIndexnowResult>} Aggregate result with per-chunk details.\n */\nexport async function run(options: NextIndexnowOptions = {}): Promise<NextIndexnowResult> {\n const startTime = Date.now();\n const chunkSize = options.chunkSize ?? CHUNK_SIZE;\n\n // 1. Validate environment & parse sitemap config\n const parsedConfig = validateEnvironment();\n\n // 2. Resolve siteUrl & key\n const { siteUrl, siteHost } = resolveSiteConfig(options, parsedConfig);\n const { key, keyLocation } = resolveKeyValue(options, siteUrl);\n\n // 3. Read sitemap\n const urls = await loadSitemapUrls(options, parsedConfig);\n\n // 4. Dry-run short-circuit\n if (options.dryRun) {\n return { urlsFound: urls.length, urlsSubmitted: 0, urlsFailed: 0, chunks: [], durationMs: Date.now() - startTime };\n }\n\n // 5. Submit URLs in chunks\n const { urlsSubmitted, urlsFailed, chunks } = await submitAllUrls(\n urls,\n siteHost,\n key,\n keyLocation,\n chunkSize,\n options.onProgress\n );\n\n return { urlsFound: urls.length, urlsSubmitted, urlsFailed, chunks, durationMs: Date.now() - startTime };\n}\n","#!/usr/bin/env node\n\n/**\n * next-indexnow — CLI entry point.\n *\n * Parses command-line arguments with commander, validates the environment,\n * reads the Next.js sitemap, and submits URLs to the IndexNow API.\n *\n * @module cli\n *\n * Usage:\n * next-indexnow\n * next-indexnow --site-url https://example.com\n * next-indexnow --key my-api-key\n * next-indexnow --sitemap ./public/sitemap-0.xml\n * next-indexnow --dry-run\n * next-indexnow --help\n */\n\nimport { readFileSync, existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport chalk from 'chalk';\nimport { Command } from 'commander';\nimport logSymbols from 'log-symbols';\nimport ora from 'ora';\n\nimport { DEFAULT_INDEXNOW_KEY } from '../constants.ts';\nimport { run } from '../index.ts';\nimport type { NextIndexnowResult, NextSitemapConfig } from '../types.ts';\nimport {\n validateNextProject,\n validateDotNext,\n validateNextSitemapConfig,\n readSitemapConfig,\n validateSiteUrl,\n ensureKeyFile,\n resolveSitemapPath,\n readSitemap,\n} from '../utils.ts';\n\n// ── ASCII Banner ───────────────────────────────────────────────────────\n\nconst BANNER = `\n${chalk.white('██╗███╗ ██╗██████╗ ███████╗██╗ ██╗███╗ ██╗ ██████╗ ██╗ ██╗')}\n${chalk.white('██║████╗ ██║██╔══██╗██╔════╝╚██╗██╔╝████╗ ██║██╔═══██╗██║ ██║')}\n${chalk.white('██║██╔██╗ ██║██║ ██║█████╗ ╚███╔╝ ██╔██╗ ██║██║ ██║██║ █╗ ██║')}\n${chalk.white('██║██║╚██╗██║██║ ██║██╔══╝ ██╔██╗ ██║╚██╗██║██║ ██║██║███╗██║')}\n${chalk.white('██║██║ ╚████║██████╔╝███████╗██╔╝ ██╗██║ ╚████║╚██████╔╝╚███╔███╔╝')}\n${chalk.white('╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══╝╚══╝')}`;\n\nconst SEPARATOR = chalk.dim('='.repeat(69));\n\n// ── Helpers ────────────────────────────────────────────────────────────\n\n/**\n * Print a checkmark or error icon with a status label and optional detail.\n *\n * @param {boolean} valid - Whether the check passed.\n * @param {string} label - The status label text.\n * @param {string} [detail] - Optional detail text shown after a colon.\n */\nfunction checkMark(valid: boolean, label: string, detail?: string): void {\n const icon = valid ? logSymbols.success : logSymbols.error;\n const msg = detail ? `${label}: ${detail}` : label;\n console.log(`${icon} ${msg}`);\n}\n\n/**\n * Print the summary footer message after a submission run.\n *\n * @param {NextIndexnowResult} result - The result from the IndexNow submission run.\n * @param {boolean} dryRun - Whether this was a dry-run preview.\n */\nfunction printFooter(result: NextIndexnowResult, dryRun: boolean): void {\n console.log('');\n if (dryRun) {\n console.log(chalk.green(`Dry-run complete. ${result.urlsFound} urls would be submitted. 🚀`));\n } else if (result.urlsFailed === 0) {\n console.log(chalk.green(`All ${result.urlsSubmitted} urls submitted to IndexNow successfully! 🥳`));\n } else {\n console.log(\n chalk.yellow(\n `${result.urlsSubmitted}/${result.urlsFound} urls submitted successfully (${result.urlsFailed} failed).`\n )\n );\n }\n}\n\n/**\n * Print detailed failure information for failed submission batches.\n *\n * @param {NextIndexnowResult} result - The result from the IndexNow submission run.\n */\nfunction printFailures(result: NextIndexnowResult): void {\n if (result.urlsFailed === 0) return;\n\n console.error(chalk.red.bold('Failed submissions:'));\n for (const chunk of result.chunks) {\n if (!chunk.success) {\n console.error(chalk.red(` ${logSymbols.error} ${chunk.error}`));\n }\n }\n}\n\n// ── Ctrl+C handling ────────────────────────────────────────────────────\n\nlet spinner: ReturnType<typeof ora> | null = null;\n\nprocess.on('SIGINT', () => {\n if (spinner?.isSpinning) {\n spinner.stop();\n }\n console.log('');\n console.log(chalk.yellow('Process aborted by user.'));\n process.exit(130);\n});\n\nconst __filename = fileURLToPath(import.meta.url);\nconst pkgPathSrc = resolve(__filename, '..', '..', '..', 'package.json');\nconst pkgPathDist = resolve(__filename, '..', '..', 'package.json');\nconst pkgPath = existsSync(pkgPathDist) ? pkgPathDist : pkgPathSrc;\n\n// Read version from package.json\nconst pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version: string };\n\nconst program = new Command();\n\nprogram\n .name('next-indexnow')\n .description('Submit Next.js sitemap URLs to the IndexNow API for faster search engine indexing')\n .version(pkg.version)\n .option('--site-url <url>', 'The site URL (e.g. https://example.com). Overrides next-sitemap.config value.')\n .option('--key <key>', 'IndexNow API key. Falls back to INDEXNOW_KEY environment variable or a built-in default key.')\n .option('--sitemap <path>', 'Path to the sitemap XML file')\n .option('--chunk-size <number>', 'URLs per submission batch', (v) => Number.parseInt(v, 10), 100)\n .option('-d, --dry-run', 'Preview URLs without submitting to the IndexNow API')\n .addHelpText(\n 'after',\n `\nExamples:\n $ next-indexnow Submit URLs using settings from next-sitemap.config\n $ next-indexnow --site-url https://example.com Override the site URL\n $ next-indexnow --key my-api-key Provide IndexNow API key\n $ next-indexnow --sitemap ./public/sitemap.xml Use a custom sitemap path\n $ next-indexnow --dry-run Preview URLs without submitting\n $ next-indexnow --help Show this help message\n `\n )\n .parse(process.argv);\n\n/**\n * Log submission results as checkmark-style lines.\n *\n * @param {NextIndexnowResult} result - The result from the IndexNow submission run.\n */\nfunction displayResults(result: NextIndexnowResult): void {\n const duration = (result.durationMs / 1000).toFixed(2);\n\n const items = [\n ['URLs found', String(result.urlsFound), result.urlsFound > 0 ? ('blue' as const) : ('dim' as const)],\n ['URLs submitted', String(result.urlsSubmitted), result.urlsSubmitted > 0 ? ('green' as const) : ('dim' as const)],\n ['URLs failed', String(result.urlsFailed), result.urlsFailed > 0 ? ('red' as const) : ('dim' as const)],\n ['Duration', `${duration}s`, result.durationMs > 0 ? ('yellow' as const) : ('dim' as const)],\n ];\n\n console.log('');\n for (const [label, value, color] of items) {\n let coloredValue: string;\n switch (color) {\n case 'red':\n coloredValue = chalk.red(value);\n break;\n case 'dim':\n coloredValue = chalk.dim(value);\n break;\n case 'green':\n coloredValue = chalk.green(value);\n break;\n case 'blue':\n coloredValue = chalk.blue(value);\n break;\n case 'yellow':\n coloredValue = chalk.yellow(value);\n break;\n default:\n coloredValue = value;\n break;\n }\n console.log(`${logSymbols.success} ${chalk.bold(label)}: ${coloredValue}`);\n }\n}\n\n/**\n * Run validation checks and display checkmarks, returning parsed state.\n *\n * @param {object} opts - CLI options for site URL and API key.\n * @param {string} [opts.siteUrl] - Override site URL from --site-url option.\n * @param {string} [opts.key] - Override API key from --key option.\n *\n * @returns {{ parsedConfig: NextSitemapConfig; siteUrl: string; siteHost: string; key: string; keyLocation: string }} Resolved configuration values.\n */\nfunction runValidationChecks(opts: { siteUrl?: string; key?: string }): {\n parsedConfig: NextSitemapConfig;\n siteUrl: string;\n siteHost: string;\n key: string;\n keyLocation: string;\n} {\n // 1. Next.js project check\n const projectCheck = validateNextProject();\n if (!projectCheck.valid) {\n checkMark(false, 'Next.js project', projectCheck.error);\n console.log(chalk.red(`\\n ${projectCheck.error}`));\n process.exit(1);\n }\n checkMark(true, 'Next.js config found');\n\n // 2. Build directory check\n const dotNextCheck = validateDotNext();\n if (!dotNextCheck.valid) {\n checkMark(false, 'Build directory', dotNextCheck.error);\n console.log(chalk.red(`\\n ${dotNextCheck.error}`));\n process.exit(1);\n }\n checkMark(true, 'Build directory exists (.next)');\n\n // 3. Sitemap config check\n const sitemapConfigCheck = validateNextSitemapConfig();\n if (!sitemapConfigCheck.valid) {\n checkMark(false, 'Sitemap config', sitemapConfigCheck.error);\n console.log(chalk.red(`\\n ${sitemapConfigCheck.error}`));\n process.exit(1);\n }\n checkMark(true, 'Sitemap config found', sitemapConfigCheck.filePath);\n\n // 4. Parse sitemap config\n const parsedConfig = readSitemapConfig(sitemapConfigCheck.filePath!);\n if (!parsedConfig) {\n checkMark(false, 'Site URL', 'Could not parse siteUrl from config');\n console.log(chalk.red(`\\n Could not parse \"siteUrl\" from ${sitemapConfigCheck.filePath}.`));\n process.exit(1);\n }\n\n // 5. Validate site URL\n const siteUrl = opts.siteUrl ?? parsedConfig.siteUrl;\n const urlValidation = validateSiteUrl(siteUrl);\n if (!urlValidation.valid) {\n checkMark(false, 'Site URL', urlValidation.error);\n console.log(chalk.red(`\\n ${urlValidation.error}`));\n process.exit(1);\n }\n const siteHost = new URL(siteUrl).host;\n checkMark(true, 'Site URL resolved', siteHost);\n\n // 6. Resolve API key\n const key = opts.key ?? process.env.INDEXNOW_KEY ?? DEFAULT_INDEXNOW_KEY;\n if (opts.key) {\n checkMark(true, 'API key provided', 'via --key option');\n } else if (process.env.INDEXNOW_KEY) {\n checkMark(true, 'API key resolved', 'via INDEXNOW_KEY env');\n } else {\n checkMark(true, 'API key resolved', 'using built-in default key');\n }\n\n // 7. Ensure key file\n const keyFileCheck = ensureKeyFile(key);\n if (!keyFileCheck.valid) {\n checkMark(false, 'Key file', keyFileCheck.error);\n console.log(chalk.red(`\\n ${keyFileCheck.error}`));\n process.exit(1);\n }\n const keyFilePath = resolve(process.cwd(), 'public', `${key}.txt`);\n const keyExists = existsSync(keyFilePath);\n checkMark(true, 'Key verification file', keyExists ? `${key}.txt exists` : `${key}.txt created`);\n\n const keyLocation = `${siteUrl}/${key}.txt`;\n\n return { parsedConfig, siteUrl, siteHost, key, keyLocation };\n}\n\n/**\n * Read sitemap and display checkmark with URL count.\n *\n * @param {object} opts - CLI options for sitemap path.\n * @param {string} [opts.sitemap] - Custom sitemap path from --sitemap option.\n * @param {NextSitemapConfig} parsedConfig - Parsed sitemap config for default outDir.\n *\n * @returns {Promise<string[]>} The list of URLs extracted from the sitemap.\n */\nasync function loadSitemapCheck(opts: { sitemap?: string }, parsedConfig: NextSitemapConfig): Promise<string[]> {\n const sitemapPath = opts.sitemap ?? resolveSitemapPath(parsedConfig.outDir);\n\n const sitemapResult = await readSitemap(sitemapPath);\n if (!sitemapResult.valid) {\n checkMark(false, 'Sitemap', sitemapResult.error);\n console.log(chalk.red(`\\n ${sitemapResult.error}`));\n process.exit(1);\n }\n\n const urlCount = sitemapResult.urls!.length;\n checkMark(true, 'Sitemap loaded', `${urlCount} URLs found`);\n\n return sitemapResult.urls!;\n}\n\n/**\n * Main CLI entry point.\n *\n * Displays the ASCII banner, runs validation checks with checkmarks,\n * submits URLs to the IndexNow API, and shows results with a footer.\n */\nexport async function main(): Promise<void> {\n const opts = program.optsWithGlobals();\n\n // 1. Show banner + separator + version\n console.log(BANNER);\n console.log('');\n console.log(SEPARATOR);\n console.log(chalk.yellow(`next-indexnow: v${pkg.version}`));\n console.log(SEPARATOR);\n console.log('');\n\n // 2. Run validation checks with checkmarks\n const { parsedConfig } = runValidationChecks({ siteUrl: opts.siteUrl, key: opts.key });\n await loadSitemapCheck({ sitemap: opts.sitemap }, parsedConfig);\n\n console.log('');\n\n // 3. Submit URLs with progress spinner\n spinner = ora({ color: 'cyan', discardStdin: false });\n\n const result = await run({\n siteUrl: opts.siteUrl,\n key: opts.key,\n sitemap: opts.sitemap,\n chunkSize: opts.chunkSize,\n dryRun: opts.dryRun ?? false,\n onProgress: ({ batch, totalBatches, urlCount }) => {\n if (spinner) {\n spinner.text = `Submitting batch ${batch}/${totalBatches} (${urlCount} URLs)`;\n if (!spinner.isSpinning) {\n spinner.start();\n }\n }\n },\n });\n\n if (spinner?.isSpinning) {\n spinner.stop();\n }\n console.log(SEPARATOR);\n console.log(`${logSymbols.success} Submission Completed 🎉`);\n console.log(SEPARATOR);\n\n // 4. Show results\n displayResults(result);\n\n // 5. Show footer\n printFooter(result, opts.dryRun);\n\n // 6. Show failure details at end\n printFailures(result);\n\n // 7. Exit with appropriate code\n if (result.urlsFailed > 0) {\n process.exit(1);\n }\n}\n\nmain().catch((error) => {\n console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));\n process.exit(1);\n});\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@vijayhardaha/next-indexnow",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to submit Next.js sitemaps to the IndexNow API for faster search engine indexing",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "lint": "eslint .",
9
+ "lint:fix": "eslint --fix .",
10
+ "format": "prettier --write --log-level error .",
11
+ "format:check": "prettier --check .",
12
+ "release": "release-it",
13
+ "release:dry": "release-it --dry-run",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage"
17
+ },
18
+ "type": "module",
19
+ "bin": {
20
+ "next-indexnow": "dist/cli.js"
21
+ },
22
+ "main": "./src/index.ts",
23
+ "module": "./src/index.ts",
24
+ "exports": {
25
+ ".": {
26
+ "import": "./src/index.ts",
27
+ "types": "./src/index.ts"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "LICENSE",
34
+ "README.md"
35
+ ],
36
+ "keywords": [
37
+ "indexnow",
38
+ "sitemap",
39
+ "seo",
40
+ "nextjs",
41
+ "cli"
42
+ ],
43
+ "author": {
44
+ "name": "Vijay Hardaha",
45
+ "url": "https://github.com/vijayhardaha"
46
+ },
47
+ "homepage": "https://github.com/vijayhardaha/npm-packages-monorepo/tree/master/packages/indexnow#readme",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/vijayhardaha/npm-packages-monorepo.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/vijayhardaha/npm-packages-monorepo/issues"
54
+ },
55
+ "license": "MIT",
56
+ "dependencies": {
57
+ "chalk": "^5.6.2",
58
+ "commander": "^15.0.0",
59
+ "log-symbols": "^7.0.1",
60
+ "ora": "^9.4.0",
61
+ "xml2js": "^0.6.2"
62
+ },
63
+ "devDependencies": {
64
+ "@types/xml2js": "^0.4.14"
65
+ }
66
+ }