indxel-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -76
- package/dist/bin.js +1128 -289
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1131 -290
- package/dist/index.js.map +1 -1
- package/package.json +12 -7
package/dist/bin.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command8 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import ora from "ora";
|
|
10
|
-
import { existsSync as
|
|
11
|
-
import { writeFile, mkdir, readFile as
|
|
12
|
-
import { join as
|
|
10
|
+
import { existsSync as existsSync3 } from "fs";
|
|
11
|
+
import { writeFile as writeFile2, mkdir as mkdir2, readFile as readFile3 } from "fs/promises";
|
|
12
|
+
import { join as join3 } from "path";
|
|
13
13
|
|
|
14
14
|
// src/detect.ts
|
|
15
15
|
import { existsSync } from "fs";
|
|
@@ -59,6 +59,105 @@ async function detectProject(cwd) {
|
|
|
59
59
|
return info;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// src/store.ts
|
|
63
|
+
import { existsSync as existsSync2 } from "fs";
|
|
64
|
+
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
65
|
+
import { join as join2 } from "path";
|
|
66
|
+
import { randomBytes } from "crypto";
|
|
67
|
+
var STORE_DIR = ".indxel";
|
|
68
|
+
var LAST_CHECK_FILE = "last-check.json";
|
|
69
|
+
var INDEXNOW_KEY_FILE = "indexnow-key.txt";
|
|
70
|
+
var CONFIG_FILE = "config.json";
|
|
71
|
+
async function saveCheckResult(cwd, summary) {
|
|
72
|
+
const storeDir = join2(cwd, STORE_DIR);
|
|
73
|
+
if (!existsSync2(storeDir)) {
|
|
74
|
+
await mkdir(storeDir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
const stored = {
|
|
77
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
78
|
+
summary: {
|
|
79
|
+
...summary,
|
|
80
|
+
// Serialize results with minimal data needed for diff
|
|
81
|
+
results: summary.results.map((r) => ({
|
|
82
|
+
page: {
|
|
83
|
+
filePath: r.page.filePath,
|
|
84
|
+
route: r.page.route,
|
|
85
|
+
hasMetadata: r.page.hasMetadata,
|
|
86
|
+
hasDynamicMetadata: r.page.hasDynamicMetadata,
|
|
87
|
+
isClientComponent: r.page.isClientComponent,
|
|
88
|
+
titleIsAbsolute: r.page.titleIsAbsolute,
|
|
89
|
+
extractedMetadata: r.page.extractedMetadata
|
|
90
|
+
},
|
|
91
|
+
validation: r.validation
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
await writeFile(
|
|
96
|
+
join2(storeDir, LAST_CHECK_FILE),
|
|
97
|
+
JSON.stringify(stored, null, 2),
|
|
98
|
+
"utf-8"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
async function loadPreviousCheck(cwd) {
|
|
102
|
+
const filePath = join2(cwd, STORE_DIR, LAST_CHECK_FILE);
|
|
103
|
+
if (!existsSync2(filePath)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const data = await readFile2(filePath, "utf-8");
|
|
108
|
+
return JSON.parse(data);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function generateIndexNowKey() {
|
|
114
|
+
return randomBytes(16).toString("hex");
|
|
115
|
+
}
|
|
116
|
+
async function saveIndexNowKey(cwd, key) {
|
|
117
|
+
const storeDir = join2(cwd, STORE_DIR);
|
|
118
|
+
if (!existsSync2(storeDir)) {
|
|
119
|
+
await mkdir(storeDir, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
await writeFile(join2(storeDir, INDEXNOW_KEY_FILE), key, "utf-8");
|
|
122
|
+
}
|
|
123
|
+
async function loadIndexNowKey(cwd) {
|
|
124
|
+
const filePath = join2(cwd, STORE_DIR, INDEXNOW_KEY_FILE);
|
|
125
|
+
if (!existsSync2(filePath)) return null;
|
|
126
|
+
try {
|
|
127
|
+
const key = (await readFile2(filePath, "utf-8")).trim();
|
|
128
|
+
return key || null;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function saveProjectConfig(cwd, config) {
|
|
134
|
+
const storeDir = join2(cwd, STORE_DIR);
|
|
135
|
+
if (!existsSync2(storeDir)) {
|
|
136
|
+
await mkdir(storeDir, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
await writeFile(
|
|
139
|
+
join2(storeDir, CONFIG_FILE),
|
|
140
|
+
JSON.stringify(config, null, 2),
|
|
141
|
+
"utf-8"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
async function loadProjectConfig(cwd) {
|
|
145
|
+
const filePath = join2(cwd, STORE_DIR, CONFIG_FILE);
|
|
146
|
+
if (!existsSync2(filePath)) return null;
|
|
147
|
+
try {
|
|
148
|
+
const data = await readFile2(filePath, "utf-8");
|
|
149
|
+
return JSON.parse(data);
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function resolveApiKey(explicit) {
|
|
155
|
+
if (explicit) return explicit;
|
|
156
|
+
if (process.env.INDXEL_API_KEY) return process.env.INDXEL_API_KEY;
|
|
157
|
+
const config = await loadProjectConfig(process.cwd());
|
|
158
|
+
return config?.apiKey ?? null;
|
|
159
|
+
}
|
|
160
|
+
|
|
62
161
|
// src/templates.ts
|
|
63
162
|
function seoConfigTemplate(isTypeScript) {
|
|
64
163
|
if (isTypeScript) {
|
|
@@ -207,47 +306,47 @@ var initCommand = new Command("init").description("Initialize indxel in your Nex
|
|
|
207
306
|
const ext = project.isTypeScript ? "ts" : "js";
|
|
208
307
|
const filesCreated = [];
|
|
209
308
|
if (!project.hasSeoConfig || opts.force) {
|
|
210
|
-
const configPath =
|
|
211
|
-
await
|
|
309
|
+
const configPath = join3(cwd, `seo.config.${ext}`);
|
|
310
|
+
await writeFile2(configPath, seoConfigTemplate(project.isTypeScript), "utf-8");
|
|
212
311
|
filesCreated.push(`seo.config.${ext}`);
|
|
213
312
|
console.log(chalk.green(" \u2713") + ` Generated seo.config.${ext}`);
|
|
214
313
|
} else {
|
|
215
314
|
console.log(chalk.dim(` - seo.config.${ext} already exists (skip)`));
|
|
216
315
|
}
|
|
217
316
|
if (!project.hasSitemap || opts.force) {
|
|
218
|
-
const sitemapPath =
|
|
219
|
-
await
|
|
317
|
+
const sitemapPath = join3(cwd, project.appDir, `sitemap.${ext}`);
|
|
318
|
+
await writeFile2(sitemapPath, sitemapTemplate(project.isTypeScript), "utf-8");
|
|
220
319
|
filesCreated.push(`${project.appDir}/sitemap.${ext}`);
|
|
221
320
|
console.log(chalk.green(" \u2713") + ` Generated ${project.appDir}/sitemap.${ext}`);
|
|
222
321
|
} else {
|
|
223
322
|
console.log(chalk.dim(` - sitemap already exists (skip)`));
|
|
224
323
|
}
|
|
225
324
|
if (!project.hasRobots || opts.force) {
|
|
226
|
-
const robotsPath =
|
|
227
|
-
await
|
|
325
|
+
const robotsPath = join3(cwd, project.appDir, `robots.${ext}`);
|
|
326
|
+
await writeFile2(robotsPath, robotsTemplate(project.isTypeScript), "utf-8");
|
|
228
327
|
filesCreated.push(`${project.appDir}/robots.${ext}`);
|
|
229
328
|
console.log(chalk.green(" \u2713") + ` Generated ${project.appDir}/robots.${ext}`);
|
|
230
329
|
} else {
|
|
231
330
|
console.log(chalk.dim(` - robots already exists (skip)`));
|
|
232
331
|
}
|
|
233
|
-
const gitDir =
|
|
234
|
-
const hasGit =
|
|
332
|
+
const gitDir = join3(cwd, ".git");
|
|
333
|
+
const hasGit = existsSync3(gitDir);
|
|
235
334
|
if (opts.hook || opts.force) {
|
|
236
335
|
if (!hasGit) {
|
|
237
336
|
console.log(chalk.yellow(" \u26A0") + " No .git directory found \u2014 skip hook install");
|
|
238
337
|
} else {
|
|
239
|
-
const hooksDir =
|
|
240
|
-
const hookPath =
|
|
241
|
-
if (
|
|
242
|
-
const existing = await
|
|
338
|
+
const hooksDir = join3(gitDir, "hooks");
|
|
339
|
+
const hookPath = join3(hooksDir, "pre-push");
|
|
340
|
+
if (existsSync3(hookPath) && !opts.force) {
|
|
341
|
+
const existing = await readFile3(hookPath, "utf-8");
|
|
243
342
|
if (existing.includes("indxel")) {
|
|
244
343
|
console.log(chalk.dim(" - pre-push hook already installed (skip)"));
|
|
245
344
|
} else {
|
|
246
345
|
console.log(chalk.yellow(" \u26A0") + " pre-push hook already exists (use --force to overwrite)");
|
|
247
346
|
}
|
|
248
347
|
} else {
|
|
249
|
-
await
|
|
250
|
-
await
|
|
348
|
+
await mkdir2(hooksDir, { recursive: true });
|
|
349
|
+
await writeFile2(hookPath, PRE_PUSH_HOOK, { mode: 493 });
|
|
251
350
|
filesCreated.push(".git/hooks/pre-push");
|
|
252
351
|
console.log(chalk.green(" \u2713") + " Installed git pre-push hook");
|
|
253
352
|
}
|
|
@@ -255,6 +354,31 @@ var initCommand = new Command("init").description("Initialize indxel in your Nex
|
|
|
255
354
|
} else if (hasGit) {
|
|
256
355
|
console.log(chalk.dim(" - Use --hook to install git pre-push guard"));
|
|
257
356
|
}
|
|
357
|
+
const existingKey = await loadIndexNowKey(cwd);
|
|
358
|
+
if (!existingKey || opts.force) {
|
|
359
|
+
const key = generateIndexNowKey();
|
|
360
|
+
const publicDir = join3(cwd, "public");
|
|
361
|
+
if (!existsSync3(publicDir)) {
|
|
362
|
+
await mkdir2(publicDir, { recursive: true });
|
|
363
|
+
}
|
|
364
|
+
await writeFile2(join3(publicDir, `${key}.txt`), key, "utf-8");
|
|
365
|
+
await saveIndexNowKey(cwd, key);
|
|
366
|
+
filesCreated.push(`public/${key}.txt`);
|
|
367
|
+
console.log(chalk.green(" \u2713") + " IndexNow ready \u2014 Bing, Yandex & Naver will pick up your pages on deploy");
|
|
368
|
+
} else {
|
|
369
|
+
const keyFile = join3(cwd, "public", `${existingKey}.txt`);
|
|
370
|
+
if (existsSync3(keyFile)) {
|
|
371
|
+
console.log(chalk.dim(" - IndexNow already set up (skip)"));
|
|
372
|
+
} else {
|
|
373
|
+
const publicDir = join3(cwd, "public");
|
|
374
|
+
if (!existsSync3(publicDir)) {
|
|
375
|
+
await mkdir2(publicDir, { recursive: true });
|
|
376
|
+
}
|
|
377
|
+
await writeFile2(keyFile, existingKey, "utf-8");
|
|
378
|
+
filesCreated.push(`public/${existingKey}.txt`);
|
|
379
|
+
console.log(chalk.green(" \u2713") + " IndexNow key file restored");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
258
382
|
console.log("");
|
|
259
383
|
if (filesCreated.length > 0) {
|
|
260
384
|
console.log(
|
|
@@ -272,54 +396,78 @@ var initCommand = new Command("init").description("Initialize indxel in your Nex
|
|
|
272
396
|
console.log(chalk.dim(" 3. Run ") + chalk.bold("npx indxel init --hook") + chalk.dim(" to guard git pushes"));
|
|
273
397
|
}
|
|
274
398
|
console.log("");
|
|
399
|
+
console.log(chalk.dim(" Want continuous monitoring?"));
|
|
400
|
+
console.log(chalk.dim(" Run ") + chalk.bold("npx indxel link") + chalk.dim(" to connect your dashboard."));
|
|
401
|
+
console.log("");
|
|
275
402
|
});
|
|
276
403
|
|
|
277
404
|
// src/commands/check.ts
|
|
278
405
|
import { Command as Command2 } from "commander";
|
|
279
|
-
import
|
|
406
|
+
import chalk4 from "chalk";
|
|
280
407
|
import ora2 from "ora";
|
|
281
408
|
import { validateMetadata } from "indxel";
|
|
282
409
|
|
|
283
410
|
// src/scanner.ts
|
|
284
|
-
import { readFile as
|
|
285
|
-
import { join as
|
|
411
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
412
|
+
import { join as join4, dirname, sep } from "path";
|
|
286
413
|
import { glob } from "glob";
|
|
287
414
|
async function scanPages(projectRoot, appDir) {
|
|
288
|
-
const appDirFull =
|
|
415
|
+
const appDirFull = join4(projectRoot, appDir);
|
|
289
416
|
const pageFiles = await glob("**/page.{tsx,ts,jsx,js}", {
|
|
290
417
|
cwd: appDirFull,
|
|
291
418
|
ignore: ["**/node_modules/**", "**/_*/**"]
|
|
292
419
|
});
|
|
293
420
|
const pages = [];
|
|
294
421
|
for (const file of pageFiles) {
|
|
295
|
-
const fullPath =
|
|
296
|
-
const content = await
|
|
422
|
+
const fullPath = join4(appDirFull, file);
|
|
423
|
+
const content = await readFile4(fullPath, "utf-8");
|
|
297
424
|
const route = filePathToRoute(file);
|
|
425
|
+
const isClient = isClientComponent(content);
|
|
298
426
|
const page = {
|
|
299
|
-
filePath:
|
|
427
|
+
filePath: join4(appDir, file),
|
|
300
428
|
route,
|
|
301
429
|
hasMetadata: false,
|
|
302
430
|
hasDynamicMetadata: false,
|
|
431
|
+
isClientComponent: isClient,
|
|
432
|
+
titleIsAbsolute: false,
|
|
303
433
|
extractedMetadata: createEmptyMetadata()
|
|
304
434
|
};
|
|
305
435
|
page.hasDynamicMetadata = hasExport(content, "generateMetadata");
|
|
306
436
|
page.hasMetadata = page.hasDynamicMetadata || hasExport(content, "metadata");
|
|
307
|
-
page.
|
|
437
|
+
if (page.hasDynamicMetadata) {
|
|
438
|
+
} else if (!isClient || page.hasMetadata) {
|
|
439
|
+
page.extractedMetadata = extractStaticMetadata(content);
|
|
440
|
+
const metaBlock = findMetadataBlock(content);
|
|
441
|
+
if (metaBlock && /absolute\s*:\s*["'`]/.test(metaBlock)) {
|
|
442
|
+
page.titleIsAbsolute = true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
308
445
|
pages.push(page);
|
|
309
446
|
}
|
|
310
447
|
const layoutFiles = await glob("**/layout.{tsx,ts,jsx,js}", {
|
|
311
448
|
cwd: appDirFull,
|
|
312
449
|
ignore: ["**/node_modules/**", "**/_*/**"]
|
|
313
450
|
});
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
const
|
|
451
|
+
const sortedLayouts = layoutFiles.sort((a, b) => {
|
|
452
|
+
const depthA = a.split(sep).length;
|
|
453
|
+
const depthB = b.split(sep).length;
|
|
454
|
+
return depthB - depthA;
|
|
455
|
+
});
|
|
456
|
+
for (const file of sortedLayouts) {
|
|
457
|
+
const fullPath = join4(appDirFull, file);
|
|
458
|
+
const content = await readFile4(fullPath, "utf-8");
|
|
317
459
|
const route = filePathToRoute(file).replace(/\/layout$/, "") || "/";
|
|
318
460
|
const hasMetadataExport = hasExport(content, "metadata") || hasExport(content, "generateMetadata");
|
|
319
461
|
if (hasMetadataExport) {
|
|
320
462
|
const layoutMeta = extractStaticMetadata(content);
|
|
463
|
+
const templateMatch = content.match(/template\s*:\s*["'`]([^"'`]+)["'`]/);
|
|
464
|
+
const titleTemplate = templateMatch?.[1] ?? null;
|
|
321
465
|
for (const page of pages) {
|
|
322
466
|
if (page.route.startsWith(route) || route === "/") {
|
|
467
|
+
if (page.extractedMetadata.title && titleTemplate && !page.titleIsAbsolute) {
|
|
468
|
+
page.extractedMetadata.title = titleTemplate.replace("%s", page.extractedMetadata.title);
|
|
469
|
+
page.titleIsAbsolute = true;
|
|
470
|
+
}
|
|
323
471
|
mergeMetadata(page.extractedMetadata, layoutMeta);
|
|
324
472
|
if (!page.hasMetadata) {
|
|
325
473
|
page.hasMetadata = true;
|
|
@@ -336,6 +484,9 @@ function filePathToRoute(filePath) {
|
|
|
336
484
|
const route = "/" + dir.split(sep).join("/");
|
|
337
485
|
return route.replace(/\/\([^)]+\)/g, "") || "/";
|
|
338
486
|
}
|
|
487
|
+
function isClientComponent(source) {
|
|
488
|
+
return /^[\s]*(['"])use client\1/.test(source);
|
|
489
|
+
}
|
|
339
490
|
function hasExport(source, name) {
|
|
340
491
|
const patterns = [
|
|
341
492
|
new RegExp(`export\\s+(const|let|var)\\s+${name}\\b`),
|
|
@@ -344,50 +495,94 @@ function hasExport(source, name) {
|
|
|
344
495
|
];
|
|
345
496
|
return patterns.some((p) => p.test(source));
|
|
346
497
|
}
|
|
498
|
+
function findMetadataBlock(source) {
|
|
499
|
+
const match = source.match(/export\s+(const|let|var)\s+metadata[\s:]/);
|
|
500
|
+
if (!match || match.index === void 0) return null;
|
|
501
|
+
const start = source.indexOf("{", match.index);
|
|
502
|
+
if (start === -1) return null;
|
|
503
|
+
let depth = 0;
|
|
504
|
+
for (let i = start; i < source.length; i++) {
|
|
505
|
+
if (source[i] === "{") depth++;
|
|
506
|
+
else if (source[i] === "}") {
|
|
507
|
+
depth--;
|
|
508
|
+
if (depth === 0) return source.substring(start, i + 1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
347
513
|
function extractStaticMetadata(source) {
|
|
348
514
|
const meta = createEmptyMetadata();
|
|
349
|
-
const
|
|
350
|
-
|
|
515
|
+
const metaBlock = findMetadataBlock(source) ?? source;
|
|
516
|
+
const absoluteMatch = metaBlock.match(
|
|
517
|
+
/absolute\s*:\s*["'`]([^"'`]+)["'`]/
|
|
351
518
|
);
|
|
352
|
-
if (
|
|
353
|
-
meta.title =
|
|
519
|
+
if (absoluteMatch) {
|
|
520
|
+
meta.title = absoluteMatch[1];
|
|
521
|
+
} else {
|
|
522
|
+
const defaultMatch = metaBlock.match(
|
|
523
|
+
/default\s*:\s*["'`]([^"'`]+)["'`]/
|
|
524
|
+
);
|
|
525
|
+
if (defaultMatch) {
|
|
526
|
+
meta.title = defaultMatch[1];
|
|
527
|
+
} else {
|
|
528
|
+
const titleMatch = metaBlock.match(
|
|
529
|
+
/(?:^|[,{\n])\s*title\s*:\s*["'`]([^"'`]+)["'`]/
|
|
530
|
+
);
|
|
531
|
+
if (titleMatch) {
|
|
532
|
+
meta.title = titleMatch[1];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
354
535
|
}
|
|
355
|
-
const descMatch =
|
|
356
|
-
/description\s*:\s*
|
|
536
|
+
const descMatch = metaBlock.match(
|
|
537
|
+
/(?:^|[,{\n])\s*description\s*:\s*\n?\s*["'`]([^"'`]+)["'`]/
|
|
357
538
|
);
|
|
358
539
|
if (descMatch) {
|
|
359
|
-
meta.description = descMatch[1]
|
|
540
|
+
meta.description = descMatch[1];
|
|
360
541
|
}
|
|
361
|
-
if (/openGraph\s*:\s*\{/.test(
|
|
362
|
-
const ogTitleMatch =
|
|
542
|
+
if (/openGraph\s*:\s*\{/.test(metaBlock)) {
|
|
543
|
+
const ogTitleMatch = metaBlock.match(
|
|
363
544
|
/openGraph\s*:\s*\{[^}]*title\s*:\s*["'`]([^"'`]+)["'`]/s
|
|
364
545
|
);
|
|
365
546
|
if (ogTitleMatch) meta.ogTitle = ogTitleMatch[1];
|
|
366
|
-
const ogDescMatch =
|
|
547
|
+
const ogDescMatch = metaBlock.match(
|
|
367
548
|
/openGraph\s*:\s*\{[^}]*description\s*:\s*["'`]([^"'`]+)["'`]/s
|
|
368
549
|
);
|
|
369
550
|
if (ogDescMatch) meta.ogDescription = ogDescMatch[1];
|
|
370
|
-
if (/images\s*:\s*\[/.test(
|
|
551
|
+
if (/images\s*:\s*\[/.test(metaBlock)) {
|
|
371
552
|
meta.ogImage = "[detected]";
|
|
372
553
|
}
|
|
373
554
|
}
|
|
374
|
-
if (/twitter\s*:\s*\{/.test(
|
|
375
|
-
const cardMatch =
|
|
555
|
+
if (/twitter\s*:\s*\{/.test(metaBlock)) {
|
|
556
|
+
const cardMatch = metaBlock.match(
|
|
376
557
|
/card\s*:\s*["'`](summary|summary_large_image)["'`]/
|
|
377
558
|
);
|
|
378
559
|
if (cardMatch) meta.twitterCard = cardMatch[1];
|
|
379
560
|
}
|
|
380
|
-
if (/robots\s*:\s*\{/.test(
|
|
381
|
-
const robotsMatch =
|
|
561
|
+
if (/robots\s*:\s*\{/.test(metaBlock) || /robots\s*:\s*["'`]/.test(metaBlock)) {
|
|
562
|
+
const robotsMatch = metaBlock.match(
|
|
382
563
|
/robots\s*:\s*["'`]([^"'`]+)["'`]/
|
|
383
564
|
);
|
|
384
565
|
if (robotsMatch) meta.robots = robotsMatch[1];
|
|
385
566
|
}
|
|
386
|
-
if (/alternates\s*:\s*\{/.test(
|
|
387
|
-
const canonicalMatch =
|
|
567
|
+
if (/alternates\s*:\s*\{/.test(metaBlock)) {
|
|
568
|
+
const canonicalMatch = metaBlock.match(
|
|
388
569
|
/canonical\s*:\s*["'`]([^"'`]+)["'`]/
|
|
389
570
|
);
|
|
390
571
|
if (canonicalMatch) meta.canonical = canonicalMatch[1];
|
|
572
|
+
if (/languages\s*:\s*\{/.test(metaBlock)) {
|
|
573
|
+
const langs = {};
|
|
574
|
+
const langMatches = metaBlock.matchAll(
|
|
575
|
+
/["'`](\w{2}(?:-\w{2})?)["'`]\s*:\s*["'`]([^"'`]+)["'`]/g
|
|
576
|
+
);
|
|
577
|
+
for (const m of langMatches) {
|
|
578
|
+
if (m[1] && m[2] && m[2].startsWith("http")) {
|
|
579
|
+
langs[m[1]] = m[2];
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (Object.keys(langs).length > 0) {
|
|
583
|
+
meta.alternates = langs;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
391
586
|
}
|
|
392
587
|
if (/application\/ld\+json/.test(source) || /generateLD/.test(source) || /JsonLD/.test(source)) {
|
|
393
588
|
meta.structuredData = [{ "@context": "https://schema.org", "@type": "detected" }];
|
|
@@ -395,7 +590,7 @@ function extractStaticMetadata(source) {
|
|
|
395
590
|
if (/viewport\s*[:=]/.test(source)) {
|
|
396
591
|
meta.viewport = "detected";
|
|
397
592
|
}
|
|
398
|
-
if (/icons\s*:\s*\{/.test(
|
|
593
|
+
if (/icons\s*:\s*\{/.test(metaBlock) || /favicon/.test(metaBlock)) {
|
|
399
594
|
meta.favicon = "detected";
|
|
400
595
|
}
|
|
401
596
|
return meta;
|
|
@@ -427,15 +622,34 @@ function mergeMetadata(target, source) {
|
|
|
427
622
|
}
|
|
428
623
|
}
|
|
429
624
|
|
|
625
|
+
// src/config.ts
|
|
626
|
+
import { existsSync as existsSync4 } from "fs";
|
|
627
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
628
|
+
import { join as join5 } from "path";
|
|
629
|
+
var CONFIG_FILES = [".indxelrc.json", ".indxelrc", "indxel.config.json"];
|
|
630
|
+
async function loadConfig(cwd) {
|
|
631
|
+
for (const file of CONFIG_FILES) {
|
|
632
|
+
const path = join5(cwd, file);
|
|
633
|
+
if (existsSync4(path)) {
|
|
634
|
+
try {
|
|
635
|
+
const content = await readFile5(path, "utf-8");
|
|
636
|
+
return JSON.parse(content);
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return {};
|
|
642
|
+
}
|
|
643
|
+
|
|
430
644
|
// src/formatter.ts
|
|
431
645
|
import chalk2 from "chalk";
|
|
432
646
|
function formatPageResult(result) {
|
|
433
647
|
const { page, validation } = result;
|
|
434
648
|
const lines = [];
|
|
435
|
-
const
|
|
649
|
+
const scoreColor3 = getScoreColor(validation.score);
|
|
436
650
|
const icon = validation.errors.length > 0 ? chalk2.red("x") : chalk2.green("\u2713");
|
|
437
651
|
lines.push(
|
|
438
|
-
` ${icon} ${chalk2.bold(page.route)} ${
|
|
652
|
+
` ${icon} ${chalk2.bold(page.route)} ${scoreColor3(`${validation.score}/100`)}`
|
|
439
653
|
);
|
|
440
654
|
for (const error of validation.errors) {
|
|
441
655
|
lines.push(` ${chalk2.red("x")} ${error.message ?? error.name}`);
|
|
@@ -453,9 +667,9 @@ function formatSummary(summary) {
|
|
|
453
667
|
lines.push("");
|
|
454
668
|
lines.push(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
455
669
|
lines.push("");
|
|
456
|
-
const
|
|
670
|
+
const scoreColor3 = getScoreColor(averageScore);
|
|
457
671
|
lines.push(
|
|
458
|
-
` Score: ${
|
|
672
|
+
` Score: ${scoreColor3(chalk2.bold(`${averageScore}/100`))} (${summary.grade})`
|
|
459
673
|
);
|
|
460
674
|
const pagesColor = passedPages === totalPages ? chalk2.green : chalk2.yellow;
|
|
461
675
|
lines.push(
|
|
@@ -466,10 +680,27 @@ function formatSummary(summary) {
|
|
|
466
680
|
lines.push(
|
|
467
681
|
chalk2.red(` ${criticalErrors} critical issue${criticalErrors > 1 ? "s" : ""}. Fix before deploying.`)
|
|
468
682
|
);
|
|
683
|
+
if (summary.optionalErrors > 0) {
|
|
684
|
+
lines.push(
|
|
685
|
+
chalk2.yellow(` ${summary.optionalErrors} optional issue${summary.optionalErrors > 1 ? "s" : ""} (won't block CI).`)
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
} else if (summary.optionalErrors > 0) {
|
|
689
|
+
lines.push("");
|
|
690
|
+
lines.push(chalk2.yellow(` ${summary.optionalErrors} optional issue${summary.optionalErrors > 1 ? "s" : ""} (won't block CI).`));
|
|
469
691
|
} else {
|
|
470
692
|
lines.push("");
|
|
471
693
|
lines.push(chalk2.green(" All pages pass. Ship it."));
|
|
472
694
|
}
|
|
695
|
+
if (summary.skippedDynamic > 0) {
|
|
696
|
+
lines.push("");
|
|
697
|
+
lines.push(
|
|
698
|
+
chalk2.cyan(
|
|
699
|
+
` ${summary.skippedDynamic} dynamic page${summary.skippedDynamic > 1 ? "s" : ""} skipped (generateMetadata).`
|
|
700
|
+
)
|
|
701
|
+
);
|
|
702
|
+
lines.push(chalk2.dim(" Run `indxel crawl <url>` for accurate scores on dynamic pages."));
|
|
703
|
+
}
|
|
473
704
|
lines.push("");
|
|
474
705
|
return lines.join("\n");
|
|
475
706
|
}
|
|
@@ -481,6 +712,8 @@ function formatJSON(summary) {
|
|
|
481
712
|
totalPages: summary.totalPages,
|
|
482
713
|
passedPages: summary.passedPages,
|
|
483
714
|
criticalErrors: summary.criticalErrors,
|
|
715
|
+
optionalErrors: summary.optionalErrors,
|
|
716
|
+
skippedDynamic: summary.skippedDynamic,
|
|
484
717
|
pages: summary.results.map((r) => ({
|
|
485
718
|
route: r.page.route,
|
|
486
719
|
file: r.page.filePath,
|
|
@@ -571,114 +804,187 @@ function formatDiff(current, previous) {
|
|
|
571
804
|
}
|
|
572
805
|
return lines.join("\n");
|
|
573
806
|
}
|
|
807
|
+
function formatSkippedPage(page) {
|
|
808
|
+
return ` ${chalk2.cyan("~")} ${chalk2.bold(page.route)} ${chalk2.dim("skipped \u2014 generateMetadata()")}`;
|
|
809
|
+
}
|
|
574
810
|
function getScoreColor(score) {
|
|
575
811
|
if (score >= 90) return chalk2.green;
|
|
576
812
|
if (score >= 70) return chalk2.yellow;
|
|
577
813
|
return chalk2.red;
|
|
578
814
|
}
|
|
579
|
-
function computeSummary(results) {
|
|
580
|
-
const totalPages = results.length;
|
|
815
|
+
function computeSummary(results, skippedDynamic = 0) {
|
|
816
|
+
const totalPages = results.length + skippedDynamic;
|
|
581
817
|
const passedPages = results.filter((r) => r.validation.errors.length === 0).length;
|
|
582
|
-
const averageScore =
|
|
583
|
-
|
|
818
|
+
const averageScore = results.length > 0 ? Math.round(results.reduce((sum, r) => sum + r.validation.score, 0) / results.length) : 0;
|
|
819
|
+
let criticalErrors = 0;
|
|
820
|
+
let optionalErrors = 0;
|
|
821
|
+
for (const r of results) {
|
|
822
|
+
for (const e of r.validation.errors) {
|
|
823
|
+
if (e.severity === "critical") criticalErrors++;
|
|
824
|
+
else optionalErrors++;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
584
827
|
let grade;
|
|
585
828
|
if (averageScore >= 90) grade = "A";
|
|
586
829
|
else if (averageScore >= 80) grade = "B";
|
|
587
830
|
else if (averageScore >= 70) grade = "C";
|
|
588
831
|
else if (averageScore >= 60) grade = "D";
|
|
589
832
|
else grade = "F";
|
|
590
|
-
return { results, totalPages, passedPages, averageScore, grade, criticalErrors };
|
|
833
|
+
return { results, totalPages, passedPages, averageScore, grade, criticalErrors, optionalErrors, skippedDynamic };
|
|
591
834
|
}
|
|
592
835
|
|
|
593
|
-
// src/
|
|
594
|
-
import
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
},
|
|
617
|
-
validation: r.validation
|
|
618
|
-
}))
|
|
836
|
+
// src/fixer.ts
|
|
837
|
+
import chalk3 from "chalk";
|
|
838
|
+
function generateFixSuggestions(results, baseUrl) {
|
|
839
|
+
const output = [];
|
|
840
|
+
const siteUrl = baseUrl ?? "https://yoursite.com";
|
|
841
|
+
for (const { page, validation } of results) {
|
|
842
|
+
if (validation.errors.length === 0 && validation.warnings.length === 0) continue;
|
|
843
|
+
const errorIds = new Set(validation.errors.map((e) => e.id));
|
|
844
|
+
const warnIds = new Set(validation.warnings.map((w) => w.id));
|
|
845
|
+
const allIds = /* @__PURE__ */ new Set([...errorIds, ...warnIds]);
|
|
846
|
+
const meta = {};
|
|
847
|
+
const extras = [];
|
|
848
|
+
if (allIds.has("title-present")) {
|
|
849
|
+
const suggestion = routeToTitle(page.route);
|
|
850
|
+
meta.title = { absolute: suggestion };
|
|
851
|
+
} else if (allIds.has("title-length")) {
|
|
852
|
+
const current = page.extractedMetadata.title ?? "";
|
|
853
|
+
const len = current.length;
|
|
854
|
+
if (len < 50) {
|
|
855
|
+
extras.push(` ${chalk3.dim("// Title is " + len + " chars \u2014 expand to 50-60")}`);
|
|
856
|
+
} else if (len > 60) {
|
|
857
|
+
extras.push(` ${chalk3.dim("// Title is " + len + " chars \u2014 shorten to 50-60")}`);
|
|
858
|
+
}
|
|
619
859
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
860
|
+
if (allIds.has("description-present")) {
|
|
861
|
+
meta.description = routeToDescription(page.route);
|
|
862
|
+
} else if (allIds.has("description-length")) {
|
|
863
|
+
const current = page.extractedMetadata.description ?? "";
|
|
864
|
+
const len = current.length;
|
|
865
|
+
if (len < 120) {
|
|
866
|
+
extras.push(` ${chalk3.dim("// Description is " + len + " chars \u2014 expand to 120-160")}`);
|
|
867
|
+
} else if (len > 160) {
|
|
868
|
+
extras.push(` ${chalk3.dim("// Description is " + len + " chars \u2014 shorten to 120-160")}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (allIds.has("canonical-url")) {
|
|
872
|
+
const canonical = `${siteUrl}${page.route === "/" ? "" : page.route}`;
|
|
873
|
+
meta.alternates = { canonical };
|
|
874
|
+
}
|
|
875
|
+
if (allIds.has("og-image")) {
|
|
876
|
+
meta.openGraph = { images: [`${siteUrl}/og-image.png`] };
|
|
877
|
+
}
|
|
878
|
+
if (allIds.has("structured-data-present")) {
|
|
879
|
+
extras.push(` ${chalk3.dim("// Add JSON-LD structured data (WebPage, Article, FAQ...)")}`);
|
|
880
|
+
extras.push(` ${chalk3.dim("// See: https://developers.google.com/search/docs/appearance/structured-data")}`);
|
|
881
|
+
}
|
|
882
|
+
if (allIds.has("twitter-card")) {
|
|
883
|
+
meta.twitter = { card: "summary_large_image" };
|
|
884
|
+
}
|
|
885
|
+
if (Object.keys(meta).length === 0 && extras.length === 0) continue;
|
|
886
|
+
output.push(chalk3.bold(` ${chalk3.cyan(page.filePath)}`));
|
|
887
|
+
if (Object.keys(meta).length > 0) {
|
|
888
|
+
const code = formatMetadataObject(meta);
|
|
889
|
+
output.push(` ${chalk3.dim("Add/update in your page file:")}`);
|
|
890
|
+
output.push("");
|
|
891
|
+
for (const line of code.split("\n")) {
|
|
892
|
+
output.push(` ${chalk3.yellow(line)}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
for (const extra of extras) {
|
|
896
|
+
output.push(extra);
|
|
897
|
+
}
|
|
898
|
+
output.push("");
|
|
631
899
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
900
|
+
return output;
|
|
901
|
+
}
|
|
902
|
+
function routeToTitle(route) {
|
|
903
|
+
if (route === "/") return "Your Site \u2014 A brief description of what you do";
|
|
904
|
+
const segments = route.split("/").filter(Boolean);
|
|
905
|
+
const last = segments[segments.length - 1];
|
|
906
|
+
const name = last.replace(/\[.*?\]/g, "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
|
|
907
|
+
return `${name || "Page"} \u2014 Your Site`;
|
|
908
|
+
}
|
|
909
|
+
function routeToDescription(route) {
|
|
910
|
+
if (route === "/") {
|
|
911
|
+
return "A clear, compelling description of your site in 120-160 characters. Include your main value proposition and a call to action.";
|
|
912
|
+
}
|
|
913
|
+
const segments = route.split("/").filter(Boolean);
|
|
914
|
+
const last = segments[segments.length - 1];
|
|
915
|
+
const name = last.replace(/\[.*?\]/g, "").replace(/-/g, " ").trim();
|
|
916
|
+
return `Learn more about ${name || "this page"}. Add a compelling 120-160 character description with your key value proposition here.`;
|
|
917
|
+
}
|
|
918
|
+
function formatMetadataObject(meta) {
|
|
919
|
+
const lines = ["export const metadata: Metadata = {"];
|
|
920
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
921
|
+
if (typeof value === "string") {
|
|
922
|
+
lines.push(` ${key}: "${value}",`);
|
|
923
|
+
} else if (typeof value === "object" && value !== null) {
|
|
924
|
+
lines.push(` ${key}: ${JSON.stringify(value, null, 2).replace(/\n/g, "\n ")},`);
|
|
925
|
+
}
|
|
637
926
|
}
|
|
927
|
+
lines.push("};");
|
|
928
|
+
return lines.join("\n");
|
|
638
929
|
}
|
|
639
930
|
|
|
640
931
|
// src/commands/check.ts
|
|
641
|
-
var checkCommand = new Command2("check").description("Audit SEO metadata for all pages in your project").option("--cwd <path>", "Project directory", process.cwd()).option("--ci", "CI/CD mode \u2014 strict, exit 1 on any error", false).option("--diff", "Compare with previous check run", false).option("--json", "Output results as JSON", false).option("--strict", "Treat warnings as errors", false).action(async (opts) => {
|
|
932
|
+
var checkCommand = new Command2("check").description("Audit SEO metadata for all pages in your project").option("--cwd <path>", "Project directory", process.cwd()).option("--ci", "CI/CD mode \u2014 strict, exit 1 on any error", false).option("--diff", "Compare with previous check run", false).option("--json", "Output results as JSON", false).option("--strict", "Treat warnings as errors", false).option("--min-score <score>", "Minimum score to pass (0-100, default: exit on any error)").option("--fix", "Show suggested metadata code to fix errors", false).action(async (opts) => {
|
|
642
933
|
const cwd = opts.cwd;
|
|
643
934
|
const isCI = opts.ci;
|
|
644
935
|
const isStrict = opts.strict || isCI;
|
|
645
936
|
const showDiff = opts.diff;
|
|
646
937
|
const jsonOutput = opts.json;
|
|
938
|
+
const showFix = opts.fix;
|
|
939
|
+
const config = await loadConfig(cwd);
|
|
940
|
+
const minScore = opts.minScore ? parseInt(opts.minScore, 10) : config.minScore ?? null;
|
|
647
941
|
const spinner = ora2("Detecting project...").start();
|
|
648
942
|
const project = await detectProject(cwd);
|
|
649
943
|
if (!project.isNextJs) {
|
|
650
944
|
spinner.fail("Not a Next.js project");
|
|
651
945
|
if (!jsonOutput) {
|
|
652
|
-
console.log(
|
|
946
|
+
console.log(chalk4.dim(" Run this command from a Next.js project root."));
|
|
653
947
|
}
|
|
654
948
|
process.exit(1);
|
|
655
949
|
}
|
|
656
950
|
if (!project.usesAppRouter) {
|
|
657
951
|
spinner.fail("App Router not detected");
|
|
658
952
|
if (!jsonOutput) {
|
|
659
|
-
console.log(
|
|
953
|
+
console.log(chalk4.dim(" indxel requires Next.js App Router (src/app or app directory)."));
|
|
660
954
|
}
|
|
661
955
|
process.exit(1);
|
|
662
956
|
}
|
|
663
957
|
spinner.text = "Scanning pages...";
|
|
664
|
-
const
|
|
665
|
-
if (
|
|
958
|
+
const allPages = await scanPages(cwd, project.appDir);
|
|
959
|
+
if (allPages.length === 0) {
|
|
666
960
|
spinner.fail("No pages found");
|
|
667
961
|
if (!jsonOutput) {
|
|
668
|
-
console.log(
|
|
962
|
+
console.log(chalk4.dim(` No page.tsx/ts files found in ${project.appDir}/`));
|
|
669
963
|
}
|
|
670
964
|
process.exit(1);
|
|
671
965
|
}
|
|
672
|
-
|
|
966
|
+
const ignoreRoutes = config.ignoreRoutes ?? [];
|
|
967
|
+
const pages = ignoreRoutes.length > 0 ? allPages.filter((p) => !ignoreRoutes.some((pattern) => matchRoute(p.route, pattern))) : allPages;
|
|
968
|
+
const ignoredCount = allPages.length - pages.length;
|
|
969
|
+
spinner.succeed(`Found ${allPages.length} page${allPages.length > 1 ? "s" : ""}${ignoredCount > 0 ? ` (${ignoredCount} ignored)` : ""}`);
|
|
970
|
+
const staticPages = pages.filter((p) => !p.hasDynamicMetadata);
|
|
971
|
+
const dynamicPages = pages.filter((p) => p.hasDynamicMetadata);
|
|
673
972
|
if (!jsonOutput) {
|
|
674
973
|
console.log("");
|
|
675
|
-
console.log(
|
|
974
|
+
console.log(chalk4.bold(` Checking ${staticPages.length} page${staticPages.length !== 1 ? "s" : ""}...`));
|
|
975
|
+
if (dynamicPages.length > 0) {
|
|
976
|
+
console.log(chalk4.dim(` (${dynamicPages.length} dynamic page${dynamicPages.length !== 1 ? "s" : ""} skipped)`));
|
|
977
|
+
}
|
|
978
|
+
if (ignoredCount > 0) {
|
|
979
|
+
console.log(chalk4.dim(` (${ignoredCount} page${ignoredCount !== 1 ? "s" : ""} excluded by ignoreRoutes)`));
|
|
980
|
+
}
|
|
676
981
|
console.log("");
|
|
677
982
|
}
|
|
678
983
|
const results = [];
|
|
679
|
-
for (const page of
|
|
984
|
+
for (const page of staticPages) {
|
|
680
985
|
const validation = validateMetadata(page.extractedMetadata, {
|
|
681
|
-
strict: isStrict
|
|
986
|
+
strict: isStrict,
|
|
987
|
+
disabledRules: config.disabledRules
|
|
682
988
|
});
|
|
683
989
|
const result = { page, validation };
|
|
684
990
|
results.push(result);
|
|
@@ -686,14 +992,20 @@ var checkCommand = new Command2("check").description("Audit SEO metadata for all
|
|
|
686
992
|
console.log(formatPageResult(result));
|
|
687
993
|
}
|
|
688
994
|
}
|
|
689
|
-
|
|
995
|
+
if (!jsonOutput && dynamicPages.length > 0) {
|
|
996
|
+
console.log("");
|
|
997
|
+
for (const page of dynamicPages) {
|
|
998
|
+
console.log(formatSkippedPage(page));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const summary = computeSummary(results, dynamicPages.length);
|
|
690
1002
|
await saveCheckResult(cwd, summary);
|
|
691
1003
|
if (showDiff && !jsonOutput) {
|
|
692
1004
|
const previous = await loadPreviousCheck(cwd);
|
|
693
1005
|
if (previous) {
|
|
694
1006
|
console.log(formatDiff(summary, previous.summary));
|
|
695
1007
|
} else {
|
|
696
|
-
console.log(
|
|
1008
|
+
console.log(chalk4.dim("\n No previous check found. Run again to see a diff.\n"));
|
|
697
1009
|
}
|
|
698
1010
|
}
|
|
699
1011
|
if (jsonOutput) {
|
|
@@ -701,14 +1013,39 @@ var checkCommand = new Command2("check").description("Audit SEO metadata for all
|
|
|
701
1013
|
} else {
|
|
702
1014
|
console.log(formatSummary(summary));
|
|
703
1015
|
}
|
|
704
|
-
if (
|
|
1016
|
+
if (showFix && !jsonOutput) {
|
|
1017
|
+
const fixes = generateFixSuggestions(results, config.baseUrl);
|
|
1018
|
+
if (fixes.length > 0) {
|
|
1019
|
+
console.log(chalk4.bold(" Suggested fixes:\n"));
|
|
1020
|
+
for (const fix of fixes) {
|
|
1021
|
+
console.log(fix);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (minScore !== null) {
|
|
1026
|
+
if (summary.averageScore < minScore) {
|
|
1027
|
+
if (!jsonOutput) {
|
|
1028
|
+
console.log(
|
|
1029
|
+
chalk4.red(` Score ${summary.averageScore} is below minimum ${minScore}.`)
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
}
|
|
1034
|
+
} else if (summary.criticalErrors > 0) {
|
|
705
1035
|
process.exit(1);
|
|
706
1036
|
}
|
|
707
1037
|
});
|
|
1038
|
+
function matchRoute(route, pattern) {
|
|
1039
|
+
if (pattern.endsWith("/*")) {
|
|
1040
|
+
const prefix = pattern.slice(0, -2);
|
|
1041
|
+
return route === prefix || route.startsWith(prefix + "/");
|
|
1042
|
+
}
|
|
1043
|
+
return route === pattern;
|
|
1044
|
+
}
|
|
708
1045
|
|
|
709
1046
|
// src/commands/crawl.ts
|
|
710
1047
|
import { Command as Command3 } from "commander";
|
|
711
|
-
import
|
|
1048
|
+
import chalk5 from "chalk";
|
|
712
1049
|
import ora3 from "ora";
|
|
713
1050
|
import {
|
|
714
1051
|
crawlSite,
|
|
@@ -718,17 +1055,22 @@ import {
|
|
|
718
1055
|
checkUrlsAgainstRobots,
|
|
719
1056
|
verifyAssets
|
|
720
1057
|
} from "indxel";
|
|
721
|
-
|
|
1058
|
+
function scoreColor(score) {
|
|
1059
|
+
if (score >= 90) return chalk5.green;
|
|
1060
|
+
if (score >= 70) return chalk5.yellow;
|
|
1061
|
+
return chalk5.red;
|
|
1062
|
+
}
|
|
1063
|
+
var crawlCommand = new Command3("crawl").description("Crawl a live site, audit every page, check sitemap, robots.txt, and assets").argument("<url>", "URL to start crawling (e.g., https://yoursite.com)").option("--max-pages <n>", "Maximum pages to crawl", "200").option("--max-depth <n>", "Maximum link depth", "5").option("--delay <ms>", "Delay between requests in ms", "200").option("--json", "Output results as JSON", false).option("--strict", "Treat warnings as errors", false).option("--skip-assets", "Skip asset verification", false).option("--skip-sitemap", "Skip sitemap check", false).option("--skip-robots", "Skip robots.txt check", false).option("--ignore <patterns>", "Comma-separated path patterns to exclude from analysis (e.g. /app/*,/admin/*)").option("--push", "Push results to Indxel dashboard", false).option("--api-key <key>", "API key for --push (or set INDXEL_API_KEY env var)").action(async (url, opts) => {
|
|
722
1064
|
const jsonOutput = opts.json;
|
|
723
1065
|
const maxPages = parseInt(opts.maxPages, 10);
|
|
724
1066
|
const maxDepth = parseInt(opts.maxDepth, 10);
|
|
725
|
-
const
|
|
1067
|
+
const delay4 = parseInt(opts.delay, 10);
|
|
726
1068
|
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
727
1069
|
url = `https://${url}`;
|
|
728
1070
|
}
|
|
729
1071
|
if (!jsonOutput) {
|
|
730
1072
|
console.log("");
|
|
731
|
-
console.log(
|
|
1073
|
+
console.log(chalk5.bold(` indxel crawl`) + chalk5.dim(` \u2014 ${url}`));
|
|
732
1074
|
console.log("");
|
|
733
1075
|
}
|
|
734
1076
|
let robotsResult = null;
|
|
@@ -739,15 +1081,15 @@ var crawlCommand = new Command3("crawl").description("Crawl a live site, audit e
|
|
|
739
1081
|
if (robotsResult.found) {
|
|
740
1082
|
robotsSpinner.succeed("robots.txt found");
|
|
741
1083
|
for (const w of robotsResult.warnings) {
|
|
742
|
-
console.log(
|
|
1084
|
+
console.log(chalk5.yellow(` \u26A0 ${w}`));
|
|
743
1085
|
}
|
|
744
1086
|
if (robotsResult.sitemapUrls.length > 0) {
|
|
745
|
-
console.log(
|
|
1087
|
+
console.log(chalk5.dim(` Sitemap references: ${robotsResult.sitemapUrls.join(", ")}`));
|
|
746
1088
|
}
|
|
747
1089
|
} else {
|
|
748
1090
|
robotsSpinner.warn("robots.txt not found");
|
|
749
1091
|
for (const e of robotsResult.errors) {
|
|
750
|
-
console.log(
|
|
1092
|
+
console.log(chalk5.dim(` ${e}`));
|
|
751
1093
|
}
|
|
752
1094
|
}
|
|
753
1095
|
console.log("");
|
|
@@ -759,7 +1101,7 @@ var crawlCommand = new Command3("crawl").description("Crawl a live site, audit e
|
|
|
759
1101
|
const crawlResult = await crawlSite(url, {
|
|
760
1102
|
maxPages,
|
|
761
1103
|
maxDepth,
|
|
762
|
-
delay,
|
|
1104
|
+
delay: delay4,
|
|
763
1105
|
strict: opts.strict,
|
|
764
1106
|
ignorePatterns,
|
|
765
1107
|
onPageCrawled: (page) => {
|
|
@@ -774,19 +1116,19 @@ var crawlCommand = new Command3("crawl").description("Crawl a live site, audit e
|
|
|
774
1116
|
console.log("");
|
|
775
1117
|
for (const page of crawlResult.pages) {
|
|
776
1118
|
if (page.error) {
|
|
777
|
-
console.log(
|
|
1119
|
+
console.log(chalk5.red(` \u2717 ${page.url}`) + chalk5.dim(` \u2014 ${page.error}`));
|
|
778
1120
|
continue;
|
|
779
1121
|
}
|
|
780
|
-
const
|
|
781
|
-
const icon = page.validation.errors.length > 0 ?
|
|
1122
|
+
const pageColor = scoreColor(page.validation.score);
|
|
1123
|
+
const icon = page.validation.errors.length > 0 ? chalk5.red("\u2717") : chalk5.green("\u2713");
|
|
782
1124
|
console.log(
|
|
783
|
-
` ${icon} ${page.url} ${
|
|
1125
|
+
` ${icon} ${page.url} ${pageColor(`${page.validation.score}/100`)}`
|
|
784
1126
|
);
|
|
785
1127
|
for (const error of page.validation.errors) {
|
|
786
|
-
console.log(
|
|
1128
|
+
console.log(chalk5.red(` \u2717 ${error.message ?? error.description}`));
|
|
787
1129
|
}
|
|
788
1130
|
for (const warning of page.validation.warnings) {
|
|
789
|
-
console.log(
|
|
1131
|
+
console.log(chalk5.yellow(` \u26A0 ${warning.message ?? warning.description}`));
|
|
790
1132
|
}
|
|
791
1133
|
}
|
|
792
1134
|
console.log("");
|
|
@@ -804,24 +1146,26 @@ var crawlCommand = new Command3("crawl").description("Crawl a live site, audit e
|
|
|
804
1146
|
crawledUrls
|
|
805
1147
|
);
|
|
806
1148
|
if (sitemapComparison.inCrawlOnly.length > 0) {
|
|
807
|
-
console.log(
|
|
1149
|
+
console.log(chalk5.yellow(` \u26A0 ${sitemapComparison.inCrawlOnly.length} crawled pages missing from sitemap:`));
|
|
808
1150
|
for (const u of sitemapComparison.inCrawlOnly.slice(0, 10)) {
|
|
809
|
-
console.log(
|
|
1151
|
+
console.log(chalk5.dim(` - ${u}`));
|
|
810
1152
|
}
|
|
811
1153
|
}
|
|
812
1154
|
if (sitemapComparison.inSitemapOnly.length > 0) {
|
|
813
|
-
|
|
1155
|
+
const hitLimit = crawlResult.totalPages >= maxPages;
|
|
1156
|
+
const label = hitLimit ? `${sitemapComparison.inSitemapOnly.length} sitemap URLs not crawled (limit: ${maxPages} \u2014 use --max-pages to increase)` : `${sitemapComparison.inSitemapOnly.length} sitemap URLs not reachable`;
|
|
1157
|
+
console.log(chalk5.yellow(` \u26A0 ${label}:`));
|
|
814
1158
|
for (const u of sitemapComparison.inSitemapOnly.slice(0, 10)) {
|
|
815
|
-
console.log(
|
|
1159
|
+
console.log(chalk5.dim(` - ${u}`));
|
|
816
1160
|
}
|
|
817
1161
|
}
|
|
818
1162
|
if (sitemapComparison.issues.length === 0) {
|
|
819
|
-
console.log(
|
|
1163
|
+
console.log(chalk5.green(` \u2713 Sitemap matches crawled pages`));
|
|
820
1164
|
}
|
|
821
1165
|
} else {
|
|
822
1166
|
sitemapSpinner.warn("sitemap.xml not found");
|
|
823
1167
|
for (const e of sitemapResult.errors) {
|
|
824
|
-
console.log(
|
|
1168
|
+
console.log(chalk5.dim(` ${e}`));
|
|
825
1169
|
}
|
|
826
1170
|
}
|
|
827
1171
|
console.log("");
|
|
@@ -836,9 +1180,9 @@ var crawlCommand = new Command3("crawl").description("Crawl a live site, audit e
|
|
|
836
1180
|
);
|
|
837
1181
|
const blocked = robotsBlockedPages.filter((c) => c.blocked);
|
|
838
1182
|
if (!jsonOutput && blocked.length > 0) {
|
|
839
|
-
console.log(
|
|
1183
|
+
console.log(chalk5.yellow(` \u26A0 ${blocked.length} crawled pages are blocked by robots.txt:`));
|
|
840
1184
|
for (const b of blocked) {
|
|
841
|
-
console.log(
|
|
1185
|
+
console.log(chalk5.dim(` - ${b.path} (${b.blockedBy})`));
|
|
842
1186
|
}
|
|
843
1187
|
console.log("");
|
|
844
1188
|
}
|
|
@@ -854,16 +1198,16 @@ var crawlCommand = new Command3("crawl").description("Crawl a live site, audit e
|
|
|
854
1198
|
const warningAssets = assetResult.checks.filter((c) => c.warning);
|
|
855
1199
|
for (const asset of brokenAssets) {
|
|
856
1200
|
console.log(
|
|
857
|
-
|
|
1201
|
+
chalk5.red(` \u2717 ${asset.type}`) + chalk5.dim(` ${asset.url}`) + chalk5.red(` \u2014 ${asset.error ?? `HTTP ${asset.status}`}`)
|
|
858
1202
|
);
|
|
859
1203
|
}
|
|
860
1204
|
for (const asset of warningAssets) {
|
|
861
1205
|
console.log(
|
|
862
|
-
|
|
1206
|
+
chalk5.yellow(` \u26A0 ${asset.type}`) + chalk5.dim(` ${asset.url}`) + chalk5.yellow(` \u2014 ${asset.warning}`)
|
|
863
1207
|
);
|
|
864
1208
|
}
|
|
865
1209
|
if (brokenAssets.length === 0 && warningAssets.length === 0) {
|
|
866
|
-
console.log(
|
|
1210
|
+
console.log(chalk5.green(` \u2713 All assets respond correctly`));
|
|
867
1211
|
}
|
|
868
1212
|
console.log("");
|
|
869
1213
|
}
|
|
@@ -871,107 +1215,136 @@ var crawlCommand = new Command3("crawl").description("Crawl a live site, audit e
|
|
|
871
1215
|
if (!jsonOutput) {
|
|
872
1216
|
const a = crawlResult.analysis;
|
|
873
1217
|
if (a.duplicateTitles.length > 0) {
|
|
874
|
-
console.log(
|
|
1218
|
+
console.log(chalk5.bold("- Duplicate titles"));
|
|
875
1219
|
for (const dup of a.duplicateTitles.slice(0, 5)) {
|
|
876
|
-
console.log(
|
|
877
|
-
for (const u of dup.urls.slice(0, 3)) console.log(
|
|
878
|
-
if (dup.urls.length > 3) console.log(
|
|
1220
|
+
console.log(chalk5.red(` \u2717 "${dup.title.length > 60 ? dup.title.slice(0, 57) + "..." : dup.title}"`) + chalk5.dim(` (${dup.urls.length} pages)`));
|
|
1221
|
+
for (const u of dup.urls.slice(0, 3)) console.log(chalk5.dim(` ${u}`));
|
|
1222
|
+
if (dup.urls.length > 3) console.log(chalk5.dim(` ...and ${dup.urls.length - 3} more`));
|
|
879
1223
|
}
|
|
880
|
-
if (a.duplicateTitles.length > 5) console.log(
|
|
1224
|
+
if (a.duplicateTitles.length > 5) console.log(chalk5.dim(` ...and ${a.duplicateTitles.length - 5} more groups`));
|
|
881
1225
|
console.log("");
|
|
882
1226
|
}
|
|
883
1227
|
if (a.duplicateDescriptions.length > 0) {
|
|
884
|
-
console.log(
|
|
1228
|
+
console.log(chalk5.bold("- Duplicate descriptions"));
|
|
885
1229
|
for (const dup of a.duplicateDescriptions.slice(0, 5)) {
|
|
886
1230
|
const desc = dup.description.length > 60 ? dup.description.slice(0, 57) + "..." : dup.description;
|
|
887
|
-
console.log(
|
|
888
|
-
for (const u of dup.urls.slice(0, 3)) console.log(
|
|
889
|
-
if (dup.urls.length > 3) console.log(
|
|
1231
|
+
console.log(chalk5.red(` \u2717 "${desc}"`) + chalk5.dim(` (${dup.urls.length} pages)`));
|
|
1232
|
+
for (const u of dup.urls.slice(0, 3)) console.log(chalk5.dim(` ${u}`));
|
|
1233
|
+
if (dup.urls.length > 3) console.log(chalk5.dim(` ...and ${dup.urls.length - 3} more`));
|
|
890
1234
|
}
|
|
891
|
-
if (a.duplicateDescriptions.length > 5) console.log(
|
|
1235
|
+
if (a.duplicateDescriptions.length > 5) console.log(chalk5.dim(` ...and ${a.duplicateDescriptions.length - 5} more groups`));
|
|
892
1236
|
console.log("");
|
|
893
1237
|
}
|
|
894
1238
|
if (a.h1Issues.length > 0) {
|
|
895
1239
|
const missing = a.h1Issues.filter((h) => h.issue === "missing");
|
|
896
1240
|
const multiple = a.h1Issues.filter((h) => h.issue === "multiple");
|
|
897
|
-
console.log(
|
|
1241
|
+
console.log(chalk5.bold("- H1 heading issues"));
|
|
898
1242
|
if (missing.length > 0) {
|
|
899
|
-
console.log(
|
|
900
|
-
for (const h of missing.slice(0, 5)) console.log(
|
|
901
|
-
if (missing.length > 5) console.log(
|
|
1243
|
+
console.log(chalk5.red(` \u2717 ${missing.length} pages missing H1`));
|
|
1244
|
+
for (const h of missing.slice(0, 5)) console.log(chalk5.dim(` ${h.url}`));
|
|
1245
|
+
if (missing.length > 5) console.log(chalk5.dim(` ...and ${missing.length - 5} more`));
|
|
902
1246
|
}
|
|
903
1247
|
if (multiple.length > 0) {
|
|
904
|
-
console.log(
|
|
905
|
-
for (const h of multiple.slice(0, 5)) console.log(
|
|
906
|
-
if (multiple.length > 5) console.log(
|
|
1248
|
+
console.log(chalk5.yellow(` \u26A0 ${multiple.length} pages with multiple H1s`));
|
|
1249
|
+
for (const h of multiple.slice(0, 5)) console.log(chalk5.dim(` ${h.url} (${h.count} H1s)`));
|
|
1250
|
+
if (multiple.length > 5) console.log(chalk5.dim(` ...and ${multiple.length - 5} more`));
|
|
907
1251
|
}
|
|
908
1252
|
console.log("");
|
|
909
1253
|
}
|
|
910
1254
|
if (a.brokenInternalLinks.length > 0) {
|
|
911
|
-
console.log(
|
|
1255
|
+
console.log(chalk5.bold("- Broken internal links"));
|
|
912
1256
|
for (const bl of a.brokenInternalLinks.slice(0, 10)) {
|
|
913
|
-
console.log(
|
|
1257
|
+
console.log(chalk5.red(` \u2717 ${bl.to}`) + chalk5.dim(` \u2190 linked from ${bl.from} (${bl.status})`));
|
|
914
1258
|
}
|
|
915
|
-
if (a.brokenInternalLinks.length > 10) console.log(
|
|
1259
|
+
if (a.brokenInternalLinks.length > 10) console.log(chalk5.dim(` ...and ${a.brokenInternalLinks.length - 10} more`));
|
|
1260
|
+
console.log("");
|
|
1261
|
+
}
|
|
1262
|
+
if (a.brokenExternalLinks.length > 0) {
|
|
1263
|
+
console.log(chalk5.bold("- Broken external links"));
|
|
1264
|
+
for (const bl of a.brokenExternalLinks.slice(0, 10)) {
|
|
1265
|
+
console.log(chalk5.red(` \u2717 ${bl.to}`) + chalk5.dim(` \u2190 linked from ${bl.from} (${bl.status})`));
|
|
1266
|
+
}
|
|
1267
|
+
if (a.brokenExternalLinks.length > 10) console.log(chalk5.dim(` ...and ${a.brokenExternalLinks.length - 10} more`));
|
|
916
1268
|
console.log("");
|
|
917
1269
|
}
|
|
918
1270
|
if (a.redirects.length > 0) {
|
|
919
|
-
console.log(
|
|
1271
|
+
console.log(chalk5.bold("- Redirect chains"));
|
|
920
1272
|
for (const r of a.redirects.slice(0, 10)) {
|
|
921
|
-
console.log(
|
|
922
|
-
for (const step of r.chain) console.log(
|
|
1273
|
+
console.log(chalk5.yellow(` \u26A0 ${r.url}`));
|
|
1274
|
+
for (const step of r.chain) console.log(chalk5.dim(` ${step}`));
|
|
923
1275
|
}
|
|
924
|
-
if (a.redirects.length > 10) console.log(
|
|
1276
|
+
if (a.redirects.length > 10) console.log(chalk5.dim(` ...and ${a.redirects.length - 10} more`));
|
|
925
1277
|
console.log("");
|
|
926
1278
|
}
|
|
927
1279
|
if (a.thinContentPages.length > 0) {
|
|
928
1280
|
const realThin = a.thinContentPages.filter((tc) => !tc.isAppPage);
|
|
929
1281
|
const appThin = a.thinContentPages.filter((tc) => tc.isAppPage);
|
|
930
1282
|
if (realThin.length > 0) {
|
|
931
|
-
console.log(
|
|
1283
|
+
console.log(chalk5.bold("- Thin content") + chalk5.dim(" (< 200 words)"));
|
|
932
1284
|
for (const tc of realThin.slice(0, 10)) {
|
|
933
|
-
console.log(
|
|
1285
|
+
console.log(chalk5.yellow(` \u26A0 ${tc.url}`) + chalk5.dim(` \u2014 ${tc.wordCount} words`));
|
|
934
1286
|
}
|
|
935
|
-
if (realThin.length > 10) console.log(
|
|
1287
|
+
if (realThin.length > 10) console.log(chalk5.dim(` ...and ${realThin.length - 10} more`));
|
|
936
1288
|
console.log("");
|
|
937
1289
|
}
|
|
938
1290
|
if (appThin.length > 0) {
|
|
939
|
-
console.log(
|
|
1291
|
+
console.log(chalk5.bold("- App/wizard pages") + chalk5.dim(" (client-rendered, low word count expected)"));
|
|
940
1292
|
for (const tc of appThin.slice(0, 5)) {
|
|
941
|
-
console.log(
|
|
1293
|
+
console.log(chalk5.dim(` \u2139 ${tc.url} \u2014 ${tc.wordCount} words`));
|
|
942
1294
|
}
|
|
943
|
-
if (appThin.length > 5) console.log(
|
|
1295
|
+
if (appThin.length > 5) console.log(chalk5.dim(` ...and ${appThin.length - 5} more`));
|
|
944
1296
|
console.log("");
|
|
945
1297
|
}
|
|
946
1298
|
}
|
|
947
1299
|
if (a.orphanPages.length > 0) {
|
|
948
|
-
console.log(
|
|
949
|
-
for (const o of a.orphanPages.slice(0, 10)) console.log(
|
|
950
|
-
if (a.orphanPages.length > 10) console.log(
|
|
1300
|
+
console.log(chalk5.bold("- Orphan pages") + chalk5.dim(" (0 internal links pointing to them)"));
|
|
1301
|
+
for (const o of a.orphanPages.slice(0, 10)) console.log(chalk5.yellow(` \u26A0 ${o}`));
|
|
1302
|
+
if (a.orphanPages.length > 10) console.log(chalk5.dim(` ...and ${a.orphanPages.length - 10} more`));
|
|
951
1303
|
console.log("");
|
|
952
1304
|
}
|
|
953
1305
|
if (a.slowestPages.length > 0 && a.slowestPages[0].responseTimeMs > 1e3) {
|
|
954
|
-
console.log(
|
|
1306
|
+
console.log(chalk5.bold("- Slowest pages"));
|
|
955
1307
|
for (const sp of a.slowestPages.filter((p) => p.responseTimeMs > 1e3).slice(0, 5)) {
|
|
956
|
-
const color = sp.responseTimeMs > 3e3 ?
|
|
957
|
-
console.log(color(` \u26A0 ${sp.url}`) +
|
|
1308
|
+
const color = sp.responseTimeMs > 3e3 ? chalk5.red : chalk5.yellow;
|
|
1309
|
+
console.log(color(` \u26A0 ${sp.url}`) + chalk5.dim(` \u2014 ${(sp.responseTimeMs / 1e3).toFixed(1)}s`));
|
|
958
1310
|
}
|
|
959
1311
|
console.log("");
|
|
960
1312
|
}
|
|
961
1313
|
if (a.structuredDataSummary.length > 0) {
|
|
962
|
-
console.log(
|
|
1314
|
+
console.log(chalk5.bold("- Structured data (JSON-LD)"));
|
|
963
1315
|
for (const sd of a.structuredDataSummary) {
|
|
964
|
-
console.log(
|
|
1316
|
+
console.log(chalk5.green(` \u2713 ${sd.type}`) + chalk5.dim(` \u2014 ${sd.count} page${sd.count > 1 ? "s" : ""}`));
|
|
965
1317
|
}
|
|
966
1318
|
const pagesWithSD = crawlResult.pages.filter((p) => !p.error && p.structuredDataTypes.length > 0).length;
|
|
967
1319
|
const pagesWithout = crawlResult.pages.filter((p) => !p.error).length - pagesWithSD;
|
|
968
1320
|
if (pagesWithout > 0) {
|
|
969
|
-
console.log(
|
|
1321
|
+
console.log(chalk5.yellow(` \u26A0 ${pagesWithout} pages without any structured data`));
|
|
970
1322
|
}
|
|
971
1323
|
console.log("");
|
|
972
1324
|
} else {
|
|
973
|
-
console.log(
|
|
974
|
-
console.log(
|
|
1325
|
+
console.log(chalk5.bold("- Structured data (JSON-LD)"));
|
|
1326
|
+
console.log(chalk5.red(` \u2717 No structured data found on any page`));
|
|
1327
|
+
console.log("");
|
|
1328
|
+
}
|
|
1329
|
+
if (a.imageAltIssues.length > 0) {
|
|
1330
|
+
console.log(chalk5.bold("- Image alt text"));
|
|
1331
|
+
for (const img of a.imageAltIssues.slice(0, 10)) {
|
|
1332
|
+
const color = img.missingAlt / img.total >= 0.5 ? chalk5.red : chalk5.yellow;
|
|
1333
|
+
const icon = img.missingAlt / img.total >= 0.5 ? "\u2717" : "\u26A0";
|
|
1334
|
+
console.log(color(` ${icon} ${img.url}`) + chalk5.dim(` \u2014 ${img.missingAlt}/${img.total} images missing alt`));
|
|
1335
|
+
}
|
|
1336
|
+
if (a.imageAltIssues.length > 10) console.log(chalk5.dim(` ...and ${a.imageAltIssues.length - 10} more`));
|
|
1337
|
+
console.log("");
|
|
1338
|
+
}
|
|
1339
|
+
if (a.brokenImages.length > 0) {
|
|
1340
|
+
console.log(chalk5.bold("- Broken images"));
|
|
1341
|
+
for (const img of a.brokenImages.slice(0, 10)) {
|
|
1342
|
+
const src = img.src.length > 80 ? img.src.slice(0, 77) + "..." : img.src;
|
|
1343
|
+
console.log(chalk5.red(` \u2717 ${src}`) + chalk5.dim(` (${img.status}) \u2014 on ${img.pages.length} page${img.pages.length > 1 ? "s" : ""}`));
|
|
1344
|
+
for (const page of img.pages.slice(0, 3)) console.log(chalk5.dim(` ${page}`));
|
|
1345
|
+
if (img.pages.length > 3) console.log(chalk5.dim(` ...and ${img.pages.length - 3} more`));
|
|
1346
|
+
}
|
|
1347
|
+
if (a.brokenImages.length > 10) console.log(chalk5.dim(` ...and ${a.brokenImages.length - 10} more`));
|
|
975
1348
|
console.log("");
|
|
976
1349
|
}
|
|
977
1350
|
}
|
|
@@ -989,33 +1362,37 @@ var crawlCommand = new Command3("crawl").description("Crawl a live site, audit e
|
|
|
989
1362
|
)
|
|
990
1363
|
);
|
|
991
1364
|
} else {
|
|
992
|
-
const
|
|
993
|
-
console.log(
|
|
1365
|
+
const summaryColor = scoreColor(crawlResult.averageScore);
|
|
1366
|
+
console.log(chalk5.bold(" \u2500\u2500\u2500 Summary \u2500\u2500\u2500"));
|
|
994
1367
|
console.log("");
|
|
995
|
-
|
|
996
|
-
console.log(`
|
|
997
|
-
console.log(`
|
|
998
|
-
console.log(`
|
|
1368
|
+
const hitLimit = crawlResult.totalPages >= maxPages;
|
|
1369
|
+
console.log(` Pages crawled: ${chalk5.bold(String(crawlResult.totalPages))}${hitLimit ? chalk5.dim(` (limit: ${maxPages} \u2014 use --max-pages to crawl more)`) : ""}`);
|
|
1370
|
+
console.log(` Average score: ${summaryColor(chalk5.bold(`${crawlResult.averageScore}/100`))} (${crawlResult.grade})`);
|
|
1371
|
+
console.log(` Errors: ${crawlResult.totalErrors > 0 ? chalk5.red(String(crawlResult.totalErrors)) : chalk5.green("0")}`);
|
|
1372
|
+
console.log(` Warnings: ${crawlResult.totalWarnings > 0 ? chalk5.yellow(String(crawlResult.totalWarnings)) : chalk5.green("0")}`);
|
|
999
1373
|
if (assetResult) {
|
|
1000
|
-
console.log(` Broken assets: ${assetResult.totalBroken > 0 ?
|
|
1374
|
+
console.log(` Broken assets: ${assetResult.totalBroken > 0 ? chalk5.red(String(assetResult.totalBroken)) : chalk5.green("0")}`);
|
|
1001
1375
|
}
|
|
1002
1376
|
if (crawlResult.skippedUrls.length > 0) {
|
|
1003
|
-
console.log(
|
|
1377
|
+
console.log(chalk5.dim(` Skipped: ${crawlResult.skippedUrls.length} URLs (over limit)`));
|
|
1004
1378
|
}
|
|
1005
1379
|
console.log("");
|
|
1006
1380
|
}
|
|
1007
1381
|
if (opts.push) {
|
|
1008
|
-
const apiKey = opts.apiKey
|
|
1382
|
+
const apiKey = await resolveApiKey(opts.apiKey);
|
|
1009
1383
|
if (!apiKey) {
|
|
1010
1384
|
if (!jsonOutput) {
|
|
1011
|
-
console.log(
|
|
1012
|
-
console.log(
|
|
1385
|
+
console.log(chalk5.yellow(" \u26A0") + " To push results to your dashboard, link your project first:");
|
|
1386
|
+
console.log("");
|
|
1387
|
+
console.log(chalk5.bold(" npx indxel link"));
|
|
1388
|
+
console.log("");
|
|
1389
|
+
console.log(chalk5.dim(" Or use --api-key / set INDXEL_API_KEY."));
|
|
1013
1390
|
console.log("");
|
|
1014
1391
|
}
|
|
1015
1392
|
} else {
|
|
1016
1393
|
const pushSpinner = jsonOutput ? null : ora3("Pushing results to Indxel...").start();
|
|
1017
1394
|
try {
|
|
1018
|
-
const pushUrl = process.env.INDXEL_API_URL || "https://
|
|
1395
|
+
const pushUrl = process.env.INDXEL_API_URL || "https://indxel.com";
|
|
1019
1396
|
const res = await fetch(`${pushUrl}/api/cli/push`, {
|
|
1020
1397
|
method: "POST",
|
|
1021
1398
|
headers: {
|
|
@@ -1049,14 +1426,14 @@ var crawlCommand = new Command3("crawl").description("Crawl a live site, audit e
|
|
|
1049
1426
|
|
|
1050
1427
|
// src/commands/keywords.ts
|
|
1051
1428
|
import { Command as Command4 } from "commander";
|
|
1052
|
-
import
|
|
1429
|
+
import chalk6 from "chalk";
|
|
1053
1430
|
import ora4 from "ora";
|
|
1054
1431
|
import { researchKeywords, crawlSite as crawlSite2, analyzeContentGaps } from "indxel";
|
|
1055
1432
|
var keywordsCommand = new Command4("keywords").description("Research keyword opportunities and find content gaps").argument("<seed>", "Seed keyword or topic to research").option("--locale <locale>", "Language locale", "en").option("--country <country>", "Country code", "us").option("--site <url>", "Site URL to analyze content gaps against").option("--max-pages <n>", "Maximum pages to crawl for gap analysis", "30").option("--json", "Output results as JSON", false).action(async (seed, opts) => {
|
|
1056
1433
|
const jsonOutput = opts.json;
|
|
1057
1434
|
if (!jsonOutput) {
|
|
1058
1435
|
console.log("");
|
|
1059
|
-
console.log(
|
|
1436
|
+
console.log(chalk6.bold(` indxel keywords`) + chalk6.dim(` \u2014 "${seed}"`));
|
|
1060
1437
|
console.log("");
|
|
1061
1438
|
}
|
|
1062
1439
|
const kwSpinner = jsonOutput ? null : ora4("Researching keywords...").start();
|
|
@@ -1068,26 +1445,26 @@ var keywordsCommand = new Command4("keywords").description("Research keyword opp
|
|
|
1068
1445
|
kwSpinner.succeed(`Found ${kwResult.totalKeywords} keywords`);
|
|
1069
1446
|
console.log("");
|
|
1070
1447
|
if (kwResult.suggestions.length > 0) {
|
|
1071
|
-
console.log(
|
|
1448
|
+
console.log(chalk6.bold(` Direct suggestions (${kwResult.suggestions.length})`));
|
|
1072
1449
|
for (const s of kwResult.suggestions) {
|
|
1073
|
-
console.log(` ${
|
|
1450
|
+
console.log(` ${chalk6.hex("#F4A261")(s.keyword)}`);
|
|
1074
1451
|
}
|
|
1075
1452
|
console.log("");
|
|
1076
1453
|
}
|
|
1077
1454
|
if (kwResult.questions.length > 0) {
|
|
1078
|
-
console.log(
|
|
1455
|
+
console.log(chalk6.bold(` Questions (${kwResult.questions.length})`));
|
|
1079
1456
|
for (const q of kwResult.questions.slice(0, 20)) {
|
|
1080
|
-
console.log(` ${
|
|
1457
|
+
console.log(` ${chalk6.cyan("?")} ${q.keyword}`);
|
|
1081
1458
|
}
|
|
1082
1459
|
console.log("");
|
|
1083
1460
|
}
|
|
1084
1461
|
if (kwResult.longTail.length > 0) {
|
|
1085
|
-
console.log(
|
|
1462
|
+
console.log(chalk6.bold(` Long-tail (${kwResult.longTail.length})`));
|
|
1086
1463
|
for (const lt of kwResult.longTail.slice(0, 20)) {
|
|
1087
|
-
console.log(
|
|
1464
|
+
console.log(chalk6.dim(` ${lt.keyword}`));
|
|
1088
1465
|
}
|
|
1089
1466
|
if (kwResult.longTail.length > 20) {
|
|
1090
|
-
console.log(
|
|
1467
|
+
console.log(chalk6.dim(` ... and ${kwResult.longTail.length - 20} more`));
|
|
1091
1468
|
}
|
|
1092
1469
|
console.log("");
|
|
1093
1470
|
}
|
|
@@ -1116,33 +1493,33 @@ var keywordsCommand = new Command4("keywords").description("Research keyword opp
|
|
|
1116
1493
|
if (!jsonOutput) {
|
|
1117
1494
|
console.log("");
|
|
1118
1495
|
console.log(
|
|
1119
|
-
|
|
1496
|
+
chalk6.bold(` Content coverage: `) + `${gapResult.totalCovered}/${gapResult.totalKeywords} keywords (${gapResult.coveragePercent}%)`
|
|
1120
1497
|
);
|
|
1121
1498
|
console.log("");
|
|
1122
1499
|
if (gapResult.gaps.length > 0) {
|
|
1123
1500
|
const highGaps = gapResult.gaps.filter((g) => g.relevance === "high");
|
|
1124
1501
|
const medGaps = gapResult.gaps.filter((g) => g.relevance === "medium");
|
|
1125
1502
|
if (highGaps.length > 0) {
|
|
1126
|
-
console.log(
|
|
1503
|
+
console.log(chalk6.bold.red(` High priority gaps (${highGaps.length})`));
|
|
1127
1504
|
for (const gap of highGaps.slice(0, 15)) {
|
|
1128
1505
|
console.log(
|
|
1129
|
-
|
|
1506
|
+
chalk6.red(` \u2717 `) + `"${gap.keyword}" \u2192 ` + chalk6.dim(`${gap.suggestedType} at ${gap.suggestedPath}`)
|
|
1130
1507
|
);
|
|
1131
1508
|
}
|
|
1132
1509
|
console.log("");
|
|
1133
1510
|
}
|
|
1134
1511
|
if (medGaps.length > 0) {
|
|
1135
|
-
console.log(
|
|
1512
|
+
console.log(chalk6.bold.yellow(` Medium priority gaps (${medGaps.length})`));
|
|
1136
1513
|
for (const gap of medGaps.slice(0, 10)) {
|
|
1137
1514
|
console.log(
|
|
1138
|
-
|
|
1515
|
+
chalk6.yellow(` \u26A0 `) + `"${gap.keyword}" \u2192 ` + chalk6.dim(`${gap.suggestedType} at ${gap.suggestedPath}`)
|
|
1139
1516
|
);
|
|
1140
1517
|
}
|
|
1141
1518
|
console.log("");
|
|
1142
1519
|
}
|
|
1143
1520
|
}
|
|
1144
1521
|
if (gapResult.gaps.length === 0) {
|
|
1145
|
-
console.log(
|
|
1522
|
+
console.log(chalk6.green(` \u2713 All keyword opportunities are covered`));
|
|
1146
1523
|
console.log("");
|
|
1147
1524
|
}
|
|
1148
1525
|
}
|
|
@@ -1160,12 +1537,14 @@ var keywordsCommand = new Command4("keywords").description("Research keyword opp
|
|
|
1160
1537
|
|
|
1161
1538
|
// src/commands/index.ts
|
|
1162
1539
|
import { Command as Command5 } from "commander";
|
|
1163
|
-
import
|
|
1540
|
+
import chalk7 from "chalk";
|
|
1164
1541
|
import ora5 from "ora";
|
|
1165
1542
|
import { fetchSitemap as fetchSitemap2, fetchRobots as fetchRobots2 } from "indxel";
|
|
1543
|
+
|
|
1544
|
+
// src/auth.ts
|
|
1166
1545
|
async function checkPlan(apiKey) {
|
|
1167
1546
|
try {
|
|
1168
|
-
const apiUrl = process.env.INDXEL_API_URL || "https://
|
|
1547
|
+
const apiUrl = process.env.INDXEL_API_URL || "https://indxel.com";
|
|
1169
1548
|
const res = await fetch(`${apiUrl}/api/cli/plan`, {
|
|
1170
1549
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1171
1550
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -1177,9 +1556,20 @@ async function checkPlan(apiKey) {
|
|
|
1177
1556
|
return null;
|
|
1178
1557
|
}
|
|
1179
1558
|
}
|
|
1180
|
-
|
|
1559
|
+
|
|
1560
|
+
// src/commands/index.ts
|
|
1561
|
+
function delay(ms) {
|
|
1562
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1563
|
+
}
|
|
1564
|
+
var indexCommand = new Command5("index").description("Submit your pages to search engines and check indexation status").argument("<url>", "Site URL (e.g., https://yoursite.com)").option("--check", "Check which pages appear indexed (Pro+)", false).option("--indexnow-key <key>", "IndexNow key (auto-detected from .indxel/ if not specified)").option("--api-key <key>", "Indxel API key (required for --check and IndexNow submission)").option("--json", "Output results as JSON", false).action(async (url, opts) => {
|
|
1181
1565
|
const jsonOutput = opts.json;
|
|
1182
|
-
const needsPaid = opts.check
|
|
1566
|
+
const needsPaid = opts.check;
|
|
1567
|
+
function log(...args) {
|
|
1568
|
+
if (!jsonOutput) console.log(...args);
|
|
1569
|
+
}
|
|
1570
|
+
function spin(text) {
|
|
1571
|
+
return jsonOutput ? null : ora5(text).start();
|
|
1572
|
+
}
|
|
1183
1573
|
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
1184
1574
|
url = `https://${url}`;
|
|
1185
1575
|
}
|
|
@@ -1187,60 +1577,50 @@ var indexCommand = new Command5("index").description("Submit your pages to searc
|
|
|
1187
1577
|
try {
|
|
1188
1578
|
baseUrl = new URL(url);
|
|
1189
1579
|
} catch {
|
|
1190
|
-
console.error(
|
|
1580
|
+
console.error(chalk7.red(" Invalid URL."));
|
|
1191
1581
|
process.exit(1);
|
|
1192
1582
|
}
|
|
1193
1583
|
const origin = baseUrl.origin;
|
|
1194
1584
|
const host = baseUrl.hostname;
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
console.log("");
|
|
1199
|
-
}
|
|
1585
|
+
log("");
|
|
1586
|
+
log(chalk7.bold(" indxel index") + chalk7.dim(` \u2014 ${origin}`));
|
|
1587
|
+
log("");
|
|
1200
1588
|
if (needsPaid) {
|
|
1201
|
-
const apiKey = opts.apiKey
|
|
1589
|
+
const apiKey = await resolveApiKey(opts.apiKey);
|
|
1202
1590
|
if (!apiKey) {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
console.log("");
|
|
1208
|
-
}
|
|
1591
|
+
log(chalk7.red(" \u2717 --check requires a linked project (Plus plan)."));
|
|
1592
|
+
log(chalk7.dim(" Run: ") + chalk7.bold("npx indxel link"));
|
|
1593
|
+
log(chalk7.dim(" Or use --api-key / set INDXEL_API_KEY."));
|
|
1594
|
+
log("");
|
|
1209
1595
|
process.exit(1);
|
|
1210
1596
|
}
|
|
1211
1597
|
const plan = await checkPlan(apiKey);
|
|
1212
1598
|
if (!plan) {
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
console.log("");
|
|
1216
|
-
}
|
|
1599
|
+
log(chalk7.red(" \u2717 Invalid API key."));
|
|
1600
|
+
log("");
|
|
1217
1601
|
process.exit(1);
|
|
1218
1602
|
}
|
|
1219
1603
|
if (plan === "FREE") {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
console.log("");
|
|
1224
|
-
}
|
|
1604
|
+
log(chalk7.red(" \u2717 Indexation check requires a Pro plan."));
|
|
1605
|
+
log(chalk7.dim(" Upgrade at https://indxel.com/pricing"));
|
|
1606
|
+
log("");
|
|
1225
1607
|
process.exit(1);
|
|
1226
1608
|
}
|
|
1227
1609
|
}
|
|
1228
|
-
const sitemapSpinner =
|
|
1610
|
+
const sitemapSpinner = spin("Fetching sitemap...");
|
|
1229
1611
|
const sitemapResult = await fetchSitemap2(origin);
|
|
1230
1612
|
const sitemapUrl = `${origin}/sitemap.xml`;
|
|
1231
1613
|
if (!sitemapResult.found || sitemapResult.urls.length === 0) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
console.log("");
|
|
1236
|
-
}
|
|
1614
|
+
sitemapSpinner?.fail("No sitemap found");
|
|
1615
|
+
log(chalk7.dim(" Create a sitemap first: ") + chalk7.bold("npx indxel init"));
|
|
1616
|
+
log("");
|
|
1237
1617
|
if (jsonOutput) {
|
|
1238
1618
|
console.log(JSON.stringify({ error: "No sitemap found" }, null, 2));
|
|
1239
1619
|
}
|
|
1240
1620
|
process.exit(1);
|
|
1241
1621
|
}
|
|
1242
|
-
|
|
1243
|
-
const robotsSpinner =
|
|
1622
|
+
sitemapSpinner?.succeed(`Found sitemap \u2014 ${sitemapResult.urls.length} URLs`);
|
|
1623
|
+
const robotsSpinner = spin("Checking robots.txt...");
|
|
1244
1624
|
const robotsResult = await fetchRobots2(origin);
|
|
1245
1625
|
let sitemapInRobots = false;
|
|
1246
1626
|
if (robotsResult.found) {
|
|
@@ -1248,26 +1628,22 @@ var indexCommand = new Command5("index").description("Submit your pages to searc
|
|
|
1248
1628
|
(s) => s.toLowerCase().includes("sitemap")
|
|
1249
1629
|
);
|
|
1250
1630
|
if (sitemapInRobots) {
|
|
1251
|
-
|
|
1631
|
+
robotsSpinner?.succeed("robots.txt references sitemap");
|
|
1252
1632
|
} else {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
console.log(chalk6.dim(` Sitemap: ${sitemapUrl}`));
|
|
1257
|
-
}
|
|
1633
|
+
robotsSpinner?.warn("robots.txt found but doesn't reference sitemap");
|
|
1634
|
+
log(chalk7.dim(` Add this to your robots.txt:`));
|
|
1635
|
+
log(chalk7.dim(` Sitemap: ${sitemapUrl}`));
|
|
1258
1636
|
}
|
|
1259
1637
|
} else {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
console.log(chalk6.dim(" Create one with: ") + chalk6.bold("npx indxel init"));
|
|
1263
|
-
}
|
|
1638
|
+
robotsSpinner?.warn("No robots.txt found");
|
|
1639
|
+
log(chalk7.dim(" Create one with: ") + chalk7.bold("npx indxel init"));
|
|
1264
1640
|
}
|
|
1265
|
-
|
|
1266
|
-
const indexNowKey = opts.indexnowKey || process.env.INDEXNOW_KEY;
|
|
1267
|
-
|
|
1641
|
+
log("");
|
|
1642
|
+
const indexNowKey = opts.indexnowKey || process.env.INDEXNOW_KEY || await loadIndexNowKey(process.cwd());
|
|
1643
|
+
const indexNowResult = [];
|
|
1268
1644
|
if (indexNowKey) {
|
|
1269
1645
|
const urls = sitemapResult.urls.map((u) => u.loc);
|
|
1270
|
-
const indexNowSpinner =
|
|
1646
|
+
const indexNowSpinner = spin("Submitting via IndexNow...");
|
|
1271
1647
|
const indexNowEngines = [
|
|
1272
1648
|
{ name: "Bing/Yandex", endpoint: "https://api.indexnow.org/indexnow" }
|
|
1273
1649
|
];
|
|
@@ -1285,39 +1661,32 @@ var indexCommand = new Command5("index").description("Submit your pages to searc
|
|
|
1285
1661
|
}),
|
|
1286
1662
|
signal: AbortSignal.timeout(15e3)
|
|
1287
1663
|
});
|
|
1288
|
-
if (res.ok || res.status ===
|
|
1664
|
+
if (res.ok || res.status === 202) {
|
|
1289
1665
|
indexNowResult.push({ submitted: true, engine: engine.name, status: res.status });
|
|
1290
|
-
|
|
1666
|
+
indexNowSpinner?.succeed(`IndexNow \u2014 ${urls.length} URLs submitted to ${engine.name}`);
|
|
1291
1667
|
} else {
|
|
1292
1668
|
indexNowResult.push({ submitted: false, engine: engine.name, status: res.status });
|
|
1293
|
-
|
|
1669
|
+
indexNowSpinner?.warn(`IndexNow \u2014 ${engine.name} returned HTTP ${res.status}`);
|
|
1294
1670
|
}
|
|
1295
1671
|
} catch (err) {
|
|
1296
1672
|
indexNowResult.push({ submitted: false, engine: engine.name });
|
|
1297
|
-
|
|
1673
|
+
indexNowSpinner?.fail(`IndexNow \u2014 ${err instanceof Error ? err.message : "failed"}`);
|
|
1298
1674
|
}
|
|
1299
1675
|
}
|
|
1300
1676
|
} else {
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
console.log(chalk6.bold(" Google Search Console"));
|
|
1312
|
-
console.log(chalk6.dim(" Google requires manual setup via Search Console:"));
|
|
1313
|
-
console.log(chalk6.dim(" 1. Go to ") + chalk6.underline("https://search.google.com/search-console"));
|
|
1314
|
-
console.log(chalk6.dim(` 2. Add & verify ${host}`));
|
|
1315
|
-
console.log(chalk6.dim(" 3. Submit your sitemap: Sitemaps > Add > sitemap.xml"));
|
|
1316
|
-
console.log("");
|
|
1317
|
-
}
|
|
1677
|
+
log(chalk7.bold(" IndexNow") + chalk7.dim(" (Bing, Yandex, DuckDuckGo)"));
|
|
1678
|
+
log(chalk7.dim(" Not configured. Run ") + chalk7.bold("npx indxel init") + chalk7.dim(" to set it up automatically."));
|
|
1679
|
+
log("");
|
|
1680
|
+
}
|
|
1681
|
+
log(chalk7.bold(" Google Search Console"));
|
|
1682
|
+
log(chalk7.dim(" Google requires manual setup via Search Console:"));
|
|
1683
|
+
log(chalk7.dim(" 1. Go to ") + chalk7.underline("https://search.google.com/search-console"));
|
|
1684
|
+
log(chalk7.dim(` 2. Add & verify ${host}`));
|
|
1685
|
+
log(chalk7.dim(" 3. Submit your sitemap: Sitemaps > Add > sitemap.xml"));
|
|
1686
|
+
log("");
|
|
1318
1687
|
let indexationResults = null;
|
|
1319
1688
|
if (opts.check) {
|
|
1320
|
-
const checkSpinner =
|
|
1689
|
+
const checkSpinner = spin("Checking indexation status...");
|
|
1321
1690
|
indexationResults = [];
|
|
1322
1691
|
const urls = sitemapResult.urls.map((u) => u.loc);
|
|
1323
1692
|
let indexedCount = 0;
|
|
@@ -1342,24 +1711,23 @@ var indexCommand = new Command5("index").description("Submit your pages to searc
|
|
|
1342
1711
|
} catch {
|
|
1343
1712
|
indexationResults.push({ url: pageUrl, indexed: false });
|
|
1344
1713
|
}
|
|
1345
|
-
await
|
|
1346
|
-
}
|
|
1347
|
-
if (checkSpinner) {
|
|
1348
|
-
checkSpinner.succeed(`Indexation: ${indexedCount}/${urls.length} pages found in Google cache`);
|
|
1714
|
+
await delay(300);
|
|
1349
1715
|
}
|
|
1716
|
+
checkSpinner?.succeed(`Indexation: ${indexedCount}/${urls.length} pages found in Google cache`);
|
|
1350
1717
|
if (!jsonOutput) {
|
|
1351
1718
|
console.log("");
|
|
1352
1719
|
const notIndexed = indexationResults.filter((r) => !r.indexed);
|
|
1353
1720
|
if (notIndexed.length > 0) {
|
|
1354
|
-
console.log(
|
|
1721
|
+
console.log(chalk7.bold(` Not indexed (${notIndexed.length})`));
|
|
1355
1722
|
for (const r of notIndexed.slice(0, 20)) {
|
|
1356
|
-
console.log(
|
|
1723
|
+
console.log(chalk7.red(" \u2717 ") + r.url);
|
|
1724
|
+
}
|
|
1725
|
+
if (notIndexed.length > 20) {
|
|
1726
|
+
console.log(chalk7.dim(` ... and ${notIndexed.length - 20} more`));
|
|
1357
1727
|
}
|
|
1358
|
-
if (notIndexed.length > 20) console.log(chalk6.dim(` ... and ${notIndexed.length - 20} more`));
|
|
1359
1728
|
console.log("");
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
console.log(chalk6.green(" \u2713 All pages appear indexed"));
|
|
1729
|
+
} else {
|
|
1730
|
+
console.log(chalk7.green(" \u2713 All pages appear indexed"));
|
|
1363
1731
|
console.log("");
|
|
1364
1732
|
}
|
|
1365
1733
|
}
|
|
@@ -1372,16 +1740,25 @@ var indexCommand = new Command5("index").description("Submit your pages to searc
|
|
|
1372
1740
|
indexation: indexationResults
|
|
1373
1741
|
}, null, 2));
|
|
1374
1742
|
} else {
|
|
1375
|
-
console.log(
|
|
1743
|
+
console.log(chalk7.bold(" \u2500\u2500\u2500 Summary \u2500\u2500\u2500"));
|
|
1376
1744
|
console.log("");
|
|
1377
1745
|
console.log(` Sitemap: ${sitemapResult.urls.length} URLs`);
|
|
1378
|
-
|
|
1746
|
+
let robotsStatus;
|
|
1747
|
+
if (!robotsResult.found) {
|
|
1748
|
+
robotsStatus = chalk7.red("\u2717 not found");
|
|
1749
|
+
} else if (sitemapInRobots) {
|
|
1750
|
+
robotsStatus = chalk7.green("\u2713 references sitemap");
|
|
1751
|
+
} else {
|
|
1752
|
+
robotsStatus = chalk7.yellow("\u26A0 missing sitemap ref");
|
|
1753
|
+
}
|
|
1754
|
+
console.log(` robots.txt: ${robotsStatus}`);
|
|
1379
1755
|
if (indexNowResult.length > 0) {
|
|
1380
1756
|
for (const r of indexNowResult) {
|
|
1381
|
-
|
|
1757
|
+
const status = r.submitted ? chalk7.green("\u2713 submitted") : chalk7.red("\u2717 failed");
|
|
1758
|
+
console.log(` IndexNow: ${status} (${r.engine})`);
|
|
1382
1759
|
}
|
|
1383
1760
|
} else {
|
|
1384
|
-
console.log(` IndexNow: ${
|
|
1761
|
+
console.log(` IndexNow: ${chalk7.dim("not configured (run npx indxel init)")}`);
|
|
1385
1762
|
}
|
|
1386
1763
|
if (indexationResults) {
|
|
1387
1764
|
const indexedCount = indexationResults.filter((r) => r.indexed).length;
|
|
@@ -1391,15 +1768,477 @@ var indexCommand = new Command5("index").description("Submit your pages to searc
|
|
|
1391
1768
|
}
|
|
1392
1769
|
});
|
|
1393
1770
|
|
|
1771
|
+
// src/commands/perf.ts
|
|
1772
|
+
import { Command as Command6 } from "commander";
|
|
1773
|
+
import chalk8 from "chalk";
|
|
1774
|
+
import ora6 from "ora";
|
|
1775
|
+
import { fetchSitemap as fetchSitemap3 } from "indxel";
|
|
1776
|
+
var THRESHOLDS = {
|
|
1777
|
+
lcp: { good: 2500, poor: 4e3 },
|
|
1778
|
+
cls: { good: 0.1, poor: 0.25 },
|
|
1779
|
+
inp: { good: 200, poor: 500 }
|
|
1780
|
+
};
|
|
1781
|
+
function ratingFor(metric, value) {
|
|
1782
|
+
const t = THRESHOLDS[metric];
|
|
1783
|
+
if (value <= t.good) return "good";
|
|
1784
|
+
if (value <= t.poor) return "needs-work";
|
|
1785
|
+
return "poor";
|
|
1786
|
+
}
|
|
1787
|
+
function colorForRating(rating) {
|
|
1788
|
+
if (rating === "good") return chalk8.green;
|
|
1789
|
+
if (rating === "needs-work") return chalk8.yellow;
|
|
1790
|
+
return chalk8.red;
|
|
1791
|
+
}
|
|
1792
|
+
function iconForRating(rating) {
|
|
1793
|
+
if (rating === "good") return chalk8.green("\u2713");
|
|
1794
|
+
if (rating === "needs-work") return chalk8.yellow("\u26A0");
|
|
1795
|
+
return chalk8.red("\u2717");
|
|
1796
|
+
}
|
|
1797
|
+
function formatMs(ms) {
|
|
1798
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(1)}s`;
|
|
1799
|
+
return `${Math.round(ms)}ms`;
|
|
1800
|
+
}
|
|
1801
|
+
function formatCls(value) {
|
|
1802
|
+
return value.toFixed(2);
|
|
1803
|
+
}
|
|
1804
|
+
function scoreColor2(score) {
|
|
1805
|
+
if (score >= 90) return chalk8.green;
|
|
1806
|
+
if (score >= 50) return chalk8.yellow;
|
|
1807
|
+
return chalk8.red;
|
|
1808
|
+
}
|
|
1809
|
+
function delay2(ms) {
|
|
1810
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1811
|
+
}
|
|
1812
|
+
async function fetchPsi(url, strategy) {
|
|
1813
|
+
const params = new URLSearchParams({
|
|
1814
|
+
url,
|
|
1815
|
+
strategy,
|
|
1816
|
+
category: "performance"
|
|
1817
|
+
});
|
|
1818
|
+
const res = await fetch(
|
|
1819
|
+
`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?${params}`,
|
|
1820
|
+
{ signal: AbortSignal.timeout(6e4) }
|
|
1821
|
+
);
|
|
1822
|
+
if (!res.ok) {
|
|
1823
|
+
const body = await res.text().catch(() => "");
|
|
1824
|
+
throw new Error(`PSI API returned HTTP ${res.status}: ${body.slice(0, 200)}`);
|
|
1825
|
+
}
|
|
1826
|
+
const data = await res.json();
|
|
1827
|
+
return parsePsiResponse(data);
|
|
1828
|
+
}
|
|
1829
|
+
function parsePsiResponse(data) {
|
|
1830
|
+
const lr = data.lighthouseResult;
|
|
1831
|
+
if (!lr) throw new Error("No lighthouseResult in PSI response");
|
|
1832
|
+
const perfScore = lr.categories?.performance?.score;
|
|
1833
|
+
if (perfScore == null) throw new Error("No performance score in PSI response");
|
|
1834
|
+
return {
|
|
1835
|
+
performanceScore: Math.round(perfScore * 100),
|
|
1836
|
+
lcp: lr.audits?.["largest-contentful-paint"]?.numericValue ?? 0,
|
|
1837
|
+
cls: lr.audits?.["cumulative-layout-shift"]?.numericValue ?? 0,
|
|
1838
|
+
inp: lr.audits?.["interaction-to-next-paint"]?.numericValue ?? 0,
|
|
1839
|
+
fcp: lr.audits?.["first-contentful-paint"]?.numericValue ?? 0,
|
|
1840
|
+
si: lr.audits?.["speed-index"]?.numericValue ?? 0,
|
|
1841
|
+
tbt: lr.audits?.["total-blocking-time"]?.numericValue ?? 0
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
function checkBudgets(metrics, budgets) {
|
|
1845
|
+
const failures = [];
|
|
1846
|
+
if (budgets.score != null && metrics.performanceScore < budgets.score) {
|
|
1847
|
+
failures.push(
|
|
1848
|
+
`Performance score ${metrics.performanceScore} is below budget ${budgets.score}`
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
if (budgets.lcp != null && metrics.lcp > budgets.lcp) {
|
|
1852
|
+
failures.push(
|
|
1853
|
+
`LCP ${formatMs(metrics.lcp)} exceeds budget ${formatMs(budgets.lcp)}`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
if (budgets.cls != null && metrics.cls > budgets.cls) {
|
|
1857
|
+
failures.push(
|
|
1858
|
+
`CLS ${formatCls(metrics.cls)} exceeds budget ${formatCls(budgets.cls)}`
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
return failures;
|
|
1862
|
+
}
|
|
1863
|
+
function printPageResult(result) {
|
|
1864
|
+
const { url, strategy, metrics, error } = result;
|
|
1865
|
+
console.log(
|
|
1866
|
+
chalk8.bold(" indxel perf") + chalk8.dim(` \u2014 ${url} (${strategy})`)
|
|
1867
|
+
);
|
|
1868
|
+
console.log("");
|
|
1869
|
+
if (error || !metrics) {
|
|
1870
|
+
console.log(chalk8.red(` \u2717 ${error ?? "Unknown error"}`));
|
|
1871
|
+
console.log("");
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
const sc = scoreColor2(metrics.performanceScore);
|
|
1875
|
+
const perfIcon = metrics.performanceScore >= 90 ? chalk8.green("\u2713") : metrics.performanceScore >= 50 ? chalk8.yellow("\u26A0") : chalk8.red("\u2717");
|
|
1876
|
+
console.log(
|
|
1877
|
+
` ${perfIcon} ${chalk8.bold("Performance")} ${sc(chalk8.bold(`${metrics.performanceScore}/100`))}`
|
|
1878
|
+
);
|
|
1879
|
+
console.log("");
|
|
1880
|
+
console.log(chalk8.bold(" Core Web Vitals"));
|
|
1881
|
+
const lcpRating = ratingFor("lcp", metrics.lcp);
|
|
1882
|
+
const clsRating = ratingFor("cls", metrics.cls);
|
|
1883
|
+
const inpRating = ratingFor("inp", metrics.inp);
|
|
1884
|
+
console.log(
|
|
1885
|
+
` ${iconForRating(lcpRating)} ${colorForRating(lcpRating)(formatMs(metrics.lcp).padEnd(9))} ${chalk8.dim("LCP Largest Contentful Paint")}`
|
|
1886
|
+
);
|
|
1887
|
+
console.log(
|
|
1888
|
+
` ${iconForRating(clsRating)} ${colorForRating(clsRating)(formatCls(metrics.cls).padEnd(9))} ${chalk8.dim("CLS Cumulative Layout Shift")}`
|
|
1889
|
+
);
|
|
1890
|
+
console.log(
|
|
1891
|
+
` ${iconForRating(inpRating)} ${colorForRating(inpRating)(formatMs(metrics.inp).padEnd(9))} ${chalk8.dim("INP Interaction to Next Paint")}`
|
|
1892
|
+
);
|
|
1893
|
+
console.log("");
|
|
1894
|
+
console.log(chalk8.bold(" Other metrics"));
|
|
1895
|
+
console.log(
|
|
1896
|
+
` ${formatMs(metrics.fcp).padEnd(9)} ${chalk8.dim("FCP First Contentful Paint")}`
|
|
1897
|
+
);
|
|
1898
|
+
console.log(
|
|
1899
|
+
` ${formatMs(metrics.si).padEnd(9)} ${chalk8.dim("SI Speed Index")}`
|
|
1900
|
+
);
|
|
1901
|
+
console.log(
|
|
1902
|
+
` ${formatMs(metrics.tbt).padEnd(9)} ${chalk8.dim("TBT Total Blocking Time")}`
|
|
1903
|
+
);
|
|
1904
|
+
console.log("");
|
|
1905
|
+
}
|
|
1906
|
+
function printMultiPageSummary(results) {
|
|
1907
|
+
const valid = results.filter((r) => r.metrics);
|
|
1908
|
+
if (valid.length === 0) return;
|
|
1909
|
+
const avgScore = Math.round(
|
|
1910
|
+
valid.reduce((s, r) => s + r.metrics.performanceScore, 0) / valid.length
|
|
1911
|
+
);
|
|
1912
|
+
const worstLcp = Math.max(...valid.map((r) => r.metrics.lcp));
|
|
1913
|
+
const worstCls = Math.max(...valid.map((r) => r.metrics.cls));
|
|
1914
|
+
const worstInp = Math.max(...valid.map((r) => r.metrics.inp));
|
|
1915
|
+
console.log(chalk8.bold(" \u2500\u2500\u2500 Summary \u2500\u2500\u2500"));
|
|
1916
|
+
console.log("");
|
|
1917
|
+
console.log(` Pages tested: ${chalk8.bold(String(results.length))}`);
|
|
1918
|
+
console.log(
|
|
1919
|
+
` Avg score: ${scoreColor2(avgScore)(chalk8.bold(`${avgScore}/100`))}`
|
|
1920
|
+
);
|
|
1921
|
+
const lcpR = ratingFor("lcp", worstLcp);
|
|
1922
|
+
const clsR = ratingFor("cls", worstCls);
|
|
1923
|
+
const inpR = ratingFor("inp", worstInp);
|
|
1924
|
+
console.log(
|
|
1925
|
+
` Worst LCP: ${colorForRating(lcpR)(formatMs(worstLcp))}`
|
|
1926
|
+
);
|
|
1927
|
+
console.log(
|
|
1928
|
+
` Worst CLS: ${colorForRating(clsR)(formatCls(worstCls))}`
|
|
1929
|
+
);
|
|
1930
|
+
console.log(
|
|
1931
|
+
` Worst INP: ${colorForRating(inpR)(formatMs(worstInp))}`
|
|
1932
|
+
);
|
|
1933
|
+
console.log("");
|
|
1934
|
+
}
|
|
1935
|
+
var perfCommand = new Command6("perf").description("Test Core Web Vitals and performance via PageSpeed Insights").argument("<url>", "URL to test (e.g., https://yoursite.com)").option(
|
|
1936
|
+
"--strategy <strategy>",
|
|
1937
|
+
"Testing strategy: mobile or desktop",
|
|
1938
|
+
"mobile"
|
|
1939
|
+
).option(
|
|
1940
|
+
"--pages <n>",
|
|
1941
|
+
"Test top N pages from sitemap (default: 1 = just the URL)",
|
|
1942
|
+
"1"
|
|
1943
|
+
).option("--json", "Output results as JSON", false).option("--budget-lcp <ms>", "Fail if LCP exceeds threshold (ms)").option("--budget-cls <score>", "Fail if CLS exceeds threshold").option("--budget-score <n>", "Fail if perf score below threshold").action(async (url, opts) => {
|
|
1944
|
+
const jsonOutput = opts.json;
|
|
1945
|
+
const strategy = opts.strategy;
|
|
1946
|
+
const pageCount = parseInt(opts.pages, 10);
|
|
1947
|
+
if (strategy !== "mobile" && strategy !== "desktop") {
|
|
1948
|
+
console.error(
|
|
1949
|
+
chalk8.red(" --strategy must be 'mobile' or 'desktop'")
|
|
1950
|
+
);
|
|
1951
|
+
process.exit(1);
|
|
1952
|
+
}
|
|
1953
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
1954
|
+
url = `https://${url}`;
|
|
1955
|
+
}
|
|
1956
|
+
try {
|
|
1957
|
+
new URL(url);
|
|
1958
|
+
} catch {
|
|
1959
|
+
console.error(chalk8.red(" Invalid URL."));
|
|
1960
|
+
process.exit(1);
|
|
1961
|
+
}
|
|
1962
|
+
let urls = [url];
|
|
1963
|
+
if (pageCount > 1) {
|
|
1964
|
+
const sitemapSpinner = jsonOutput ? null : ora6("Fetching sitemap...").start();
|
|
1965
|
+
const sitemap = await fetchSitemap3(url);
|
|
1966
|
+
if (sitemap.found && sitemap.urls.length > 0) {
|
|
1967
|
+
urls = sitemap.urls.slice(0, pageCount).map((u) => u.loc);
|
|
1968
|
+
sitemapSpinner?.succeed(
|
|
1969
|
+
`Found ${sitemap.urls.length} URLs, testing top ${urls.length}`
|
|
1970
|
+
);
|
|
1971
|
+
} else {
|
|
1972
|
+
sitemapSpinner?.warn("No sitemap found, testing single URL");
|
|
1973
|
+
}
|
|
1974
|
+
if (!jsonOutput) console.log("");
|
|
1975
|
+
}
|
|
1976
|
+
const results = [];
|
|
1977
|
+
for (let i = 0; i < urls.length; i++) {
|
|
1978
|
+
const targetUrl = urls[i];
|
|
1979
|
+
const spinner = jsonOutput ? null : ora6(
|
|
1980
|
+
urls.length > 1 ? `Testing ${i + 1}/${urls.length}: ${targetUrl}` : `Testing ${targetUrl} (${strategy})...`
|
|
1981
|
+
).start();
|
|
1982
|
+
try {
|
|
1983
|
+
const metrics = await fetchPsi(targetUrl, strategy);
|
|
1984
|
+
spinner?.stop();
|
|
1985
|
+
results.push({ url: targetUrl, strategy, metrics });
|
|
1986
|
+
if (!jsonOutput) {
|
|
1987
|
+
printPageResult({ url: targetUrl, strategy, metrics });
|
|
1988
|
+
}
|
|
1989
|
+
} catch (err) {
|
|
1990
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1991
|
+
spinner?.fail(`Failed: ${targetUrl}`);
|
|
1992
|
+
results.push({ url: targetUrl, strategy, metrics: null, error: errMsg });
|
|
1993
|
+
if (!jsonOutput) {
|
|
1994
|
+
console.log(chalk8.red(` ${errMsg}`));
|
|
1995
|
+
console.log("");
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (i < urls.length - 1) {
|
|
1999
|
+
await delay2(2e3);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
if (!jsonOutput && results.length > 1) {
|
|
2003
|
+
printMultiPageSummary(results);
|
|
2004
|
+
}
|
|
2005
|
+
if (jsonOutput) {
|
|
2006
|
+
const output = results.length === 1 ? {
|
|
2007
|
+
url: results[0].url,
|
|
2008
|
+
strategy: results[0].strategy,
|
|
2009
|
+
...results[0].metrics ?? {},
|
|
2010
|
+
error: results[0].error ?? void 0
|
|
2011
|
+
} : {
|
|
2012
|
+
strategy,
|
|
2013
|
+
pages: results.map((r) => ({
|
|
2014
|
+
url: r.url,
|
|
2015
|
+
...r.metrics ?? {},
|
|
2016
|
+
error: r.error ?? void 0
|
|
2017
|
+
})),
|
|
2018
|
+
summary: (() => {
|
|
2019
|
+
const valid = results.filter((r) => r.metrics);
|
|
2020
|
+
if (valid.length === 0) return null;
|
|
2021
|
+
return {
|
|
2022
|
+
avgScore: Math.round(
|
|
2023
|
+
valid.reduce(
|
|
2024
|
+
(s, r) => s + r.metrics.performanceScore,
|
|
2025
|
+
0
|
|
2026
|
+
) / valid.length
|
|
2027
|
+
),
|
|
2028
|
+
worstLcp: Math.max(
|
|
2029
|
+
...valid.map((r) => r.metrics.lcp)
|
|
2030
|
+
),
|
|
2031
|
+
worstCls: Math.max(
|
|
2032
|
+
...valid.map((r) => r.metrics.cls)
|
|
2033
|
+
),
|
|
2034
|
+
worstInp: Math.max(
|
|
2035
|
+
...valid.map((r) => r.metrics.inp)
|
|
2036
|
+
)
|
|
2037
|
+
};
|
|
2038
|
+
})()
|
|
2039
|
+
};
|
|
2040
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2041
|
+
}
|
|
2042
|
+
const budgets = {
|
|
2043
|
+
lcp: opts.budgetLcp ? parseFloat(opts.budgetLcp) : void 0,
|
|
2044
|
+
cls: opts.budgetCls ? parseFloat(opts.budgetCls) : void 0,
|
|
2045
|
+
score: opts.budgetScore ? parseInt(opts.budgetScore, 10) : void 0
|
|
2046
|
+
};
|
|
2047
|
+
const hasBudgets = budgets.lcp != null || budgets.cls != null || budgets.score != null;
|
|
2048
|
+
if (hasBudgets) {
|
|
2049
|
+
const allFailures = [];
|
|
2050
|
+
for (const r of results) {
|
|
2051
|
+
if (!r.metrics) continue;
|
|
2052
|
+
const failures = checkBudgets(r.metrics, budgets);
|
|
2053
|
+
if (failures.length > 0) {
|
|
2054
|
+
allFailures.push(...failures.map((f) => `${r.url}: ${f}`));
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
if (allFailures.length > 0) {
|
|
2058
|
+
if (!jsonOutput) {
|
|
2059
|
+
console.log(chalk8.red(chalk8.bold(" Budget exceeded:")));
|
|
2060
|
+
for (const f of allFailures) {
|
|
2061
|
+
console.log(chalk8.red(` \u2717 ${f}`));
|
|
2062
|
+
}
|
|
2063
|
+
console.log("");
|
|
2064
|
+
}
|
|
2065
|
+
process.exit(1);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
// src/commands/link.ts
|
|
2071
|
+
import { Command as Command7 } from "commander";
|
|
2072
|
+
import chalk9 from "chalk";
|
|
2073
|
+
import ora7 from "ora";
|
|
2074
|
+
function delay3(ms) {
|
|
2075
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
2076
|
+
}
|
|
2077
|
+
async function openBrowser(url) {
|
|
2078
|
+
const { platform } = process;
|
|
2079
|
+
const { exec } = await import("child_process");
|
|
2080
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
2081
|
+
exec(`${cmd} ${url}`);
|
|
2082
|
+
}
|
|
2083
|
+
var linkCommand = new Command7("link").description("Link this project to your Indxel dashboard for monitoring").option("--api-key <key>", "Link directly with an API key (skip browser flow)").action(async (opts) => {
|
|
2084
|
+
const apiUrl = process.env.INDXEL_API_URL || "https://indxel.com";
|
|
2085
|
+
const cwd = process.cwd();
|
|
2086
|
+
console.log("");
|
|
2087
|
+
console.log(chalk9.bold(" indxel link"));
|
|
2088
|
+
console.log("");
|
|
2089
|
+
const existing = await loadProjectConfig(cwd);
|
|
2090
|
+
if (existing) {
|
|
2091
|
+
console.log(chalk9.green(" \u2713") + ` Already linked to ${chalk9.bold(existing.projectName)}`);
|
|
2092
|
+
console.log(chalk9.dim(` Project ID: ${existing.projectId}`));
|
|
2093
|
+
console.log(chalk9.dim(` Linked at: ${existing.linkedAt}`));
|
|
2094
|
+
console.log("");
|
|
2095
|
+
console.log(chalk9.dim(" To re-link, delete .indxel/config.json and run again."));
|
|
2096
|
+
console.log("");
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
if (opts.apiKey) {
|
|
2100
|
+
const spinner = ora7("Verifying API key...").start();
|
|
2101
|
+
try {
|
|
2102
|
+
const res = await fetch(`${apiUrl}/api/projects/by-key`, {
|
|
2103
|
+
headers: { Authorization: `Bearer ${opts.apiKey}` },
|
|
2104
|
+
signal: AbortSignal.timeout(1e4)
|
|
2105
|
+
});
|
|
2106
|
+
if (!res.ok) {
|
|
2107
|
+
spinner.fail("Invalid API key.");
|
|
2108
|
+
console.log(chalk9.dim(" Check your key at https://indxel.com/dashboard/settings"));
|
|
2109
|
+
console.log("");
|
|
2110
|
+
process.exit(1);
|
|
2111
|
+
}
|
|
2112
|
+
const body = await res.json();
|
|
2113
|
+
const project = body.project;
|
|
2114
|
+
spinner.succeed(`Linked to ${chalk9.bold(project.name)}`);
|
|
2115
|
+
await saveProjectConfig(cwd, {
|
|
2116
|
+
apiKey: opts.apiKey,
|
|
2117
|
+
projectId: project.id,
|
|
2118
|
+
projectName: project.name,
|
|
2119
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2120
|
+
});
|
|
2121
|
+
await syncIndexNowKey(cwd, apiUrl, opts.apiKey, project.id);
|
|
2122
|
+
console.log("");
|
|
2123
|
+
console.log(chalk9.dim(" Config saved to .indxel/config.json"));
|
|
2124
|
+
console.log(chalk9.dim(" You can now use ") + chalk9.bold("npx indxel crawl --push") + chalk9.dim(" without --api-key."));
|
|
2125
|
+
console.log("");
|
|
2126
|
+
return;
|
|
2127
|
+
} catch (err) {
|
|
2128
|
+
spinner.fail(err instanceof Error ? err.message : "Connection failed");
|
|
2129
|
+
console.log("");
|
|
2130
|
+
process.exit(1);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
const initSpinner = ora7("Starting device flow...").start();
|
|
2134
|
+
let deviceCode;
|
|
2135
|
+
let userCode;
|
|
2136
|
+
try {
|
|
2137
|
+
const res = await fetch(`${apiUrl}/api/cli/auth`, {
|
|
2138
|
+
method: "POST",
|
|
2139
|
+
headers: { "Content-Type": "application/json" },
|
|
2140
|
+
signal: AbortSignal.timeout(1e4)
|
|
2141
|
+
});
|
|
2142
|
+
if (!res.ok) {
|
|
2143
|
+
initSpinner.fail("Could not start device flow.");
|
|
2144
|
+
console.log(chalk9.dim(" You can also link directly: ") + chalk9.bold("npx indxel link --api-key <your-key>"));
|
|
2145
|
+
console.log("");
|
|
2146
|
+
process.exit(1);
|
|
2147
|
+
}
|
|
2148
|
+
const data = await res.json();
|
|
2149
|
+
deviceCode = data.deviceCode;
|
|
2150
|
+
userCode = data.userCode;
|
|
2151
|
+
initSpinner.stop();
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
initSpinner.fail(err instanceof Error ? err.message : "Connection failed");
|
|
2154
|
+
console.log(chalk9.dim(" You can also link directly: ") + chalk9.bold("npx indxel link --api-key <your-key>"));
|
|
2155
|
+
console.log("");
|
|
2156
|
+
process.exit(1);
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const connectUrl = `${apiUrl}/cli/connect?code=${userCode}`;
|
|
2160
|
+
console.log(chalk9.bold(" Open this URL in your browser:"));
|
|
2161
|
+
console.log("");
|
|
2162
|
+
console.log(` ${chalk9.underline(connectUrl)}`);
|
|
2163
|
+
console.log("");
|
|
2164
|
+
console.log(` Your code: ${chalk9.bold.cyan(userCode)}`);
|
|
2165
|
+
console.log("");
|
|
2166
|
+
try {
|
|
2167
|
+
await openBrowser(connectUrl);
|
|
2168
|
+
console.log(chalk9.dim(" Browser opened automatically."));
|
|
2169
|
+
} catch {
|
|
2170
|
+
}
|
|
2171
|
+
const pollSpinner = ora7("Waiting for authorization...").start();
|
|
2172
|
+
const maxWait = 5 * 60 * 1e3;
|
|
2173
|
+
const pollInterval = 2e3;
|
|
2174
|
+
const startTime = Date.now();
|
|
2175
|
+
while (Date.now() - startTime < maxWait) {
|
|
2176
|
+
await delay3(pollInterval);
|
|
2177
|
+
try {
|
|
2178
|
+
const res = await fetch(`${apiUrl}/api/cli/auth?code=${deviceCode}`, {
|
|
2179
|
+
signal: AbortSignal.timeout(1e4)
|
|
2180
|
+
});
|
|
2181
|
+
if (res.status === 202) {
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
2184
|
+
if (res.ok) {
|
|
2185
|
+
const data = await res.json();
|
|
2186
|
+
pollSpinner.succeed(`Linked to ${chalk9.bold(data.projectName)}`);
|
|
2187
|
+
await saveProjectConfig(cwd, {
|
|
2188
|
+
apiKey: data.apiKey,
|
|
2189
|
+
projectId: data.projectId,
|
|
2190
|
+
projectName: data.projectName,
|
|
2191
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2192
|
+
});
|
|
2193
|
+
await syncIndexNowKey(cwd, apiUrl, data.apiKey, data.projectId);
|
|
2194
|
+
console.log("");
|
|
2195
|
+
console.log(chalk9.dim(" Config saved to .indxel/config.json"));
|
|
2196
|
+
console.log(chalk9.dim(" You can now use ") + chalk9.bold("npx indxel crawl --push") + chalk9.dim(" without --api-key."));
|
|
2197
|
+
console.log("");
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
pollSpinner.fail("Authorization failed.");
|
|
2201
|
+
console.log("");
|
|
2202
|
+
process.exit(1);
|
|
2203
|
+
} catch {
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
pollSpinner.fail("Timed out waiting for authorization (5 minutes).");
|
|
2207
|
+
console.log(chalk9.dim(" Try again: ") + chalk9.bold("npx indxel link"));
|
|
2208
|
+
console.log("");
|
|
2209
|
+
process.exit(1);
|
|
2210
|
+
});
|
|
2211
|
+
async function syncIndexNowKey(cwd, apiUrl, apiKey, projectId) {
|
|
2212
|
+
const indexNowKey = await loadIndexNowKey(cwd);
|
|
2213
|
+
if (!indexNowKey) return;
|
|
2214
|
+
try {
|
|
2215
|
+
const res = await fetch(`${apiUrl}/api/projects/${projectId}/indexation/setup`, {
|
|
2216
|
+
method: "POST",
|
|
2217
|
+
headers: {
|
|
2218
|
+
"Content-Type": "application/json",
|
|
2219
|
+
Authorization: `Bearer ${apiKey}`
|
|
2220
|
+
},
|
|
2221
|
+
body: JSON.stringify({ action: "sync", key: indexNowKey }),
|
|
2222
|
+
signal: AbortSignal.timeout(1e4)
|
|
2223
|
+
});
|
|
2224
|
+
if (res.ok) {
|
|
2225
|
+
console.log(chalk9.green(" \u2713") + " IndexNow key synced to dashboard");
|
|
2226
|
+
}
|
|
2227
|
+
} catch {
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
1394
2231
|
// src/index.ts
|
|
1395
2232
|
function createProgram() {
|
|
1396
|
-
const program2 = new
|
|
2233
|
+
const program2 = new Command8();
|
|
1397
2234
|
program2.name("indxel").description("Infrastructure SEO developer-first. ESLint pour le SEO.").version("0.1.0");
|
|
1398
2235
|
program2.addCommand(initCommand);
|
|
1399
2236
|
program2.addCommand(checkCommand);
|
|
1400
2237
|
program2.addCommand(crawlCommand);
|
|
1401
2238
|
program2.addCommand(keywordsCommand);
|
|
1402
2239
|
program2.addCommand(indexCommand);
|
|
2240
|
+
program2.addCommand(perfCommand);
|
|
2241
|
+
program2.addCommand(linkCommand);
|
|
1403
2242
|
return program2;
|
|
1404
2243
|
}
|
|
1405
2244
|
|