@edtools/cli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-JCUQ7D56.js +688 -0
- package/dist/cli/commands/generate.d.ts +2 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +114 -38
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/validate.d.ts +9 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +196 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.js +534 -46
- package/dist/cli/index.js.map +1 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +31 -5
- package/dist/core/generator.js.map +1 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +1 -1
- package/dist/types/content.d.ts +44 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/ui/banner.d.ts.map +1 -1
- package/dist/ui/banner.js +9 -1
- package/dist/ui/banner.js.map +1 -1
- package/dist/utils/seo-validator.d.ts +35 -0
- package/dist/utils/seo-validator.d.ts.map +1 -0
- package/dist/utils/seo-validator.js +244 -0
- package/dist/utils/seo-validator.js.map +1 -0
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ContentGenerator
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-JCUQ7D56.js";
|
|
5
5
|
import "../chunk-INVECVSW.js";
|
|
6
6
|
|
|
7
7
|
// src/cli/index.ts
|
|
8
8
|
import { Command } from "commander";
|
|
9
|
-
import
|
|
9
|
+
import chalk6 from "chalk";
|
|
10
10
|
|
|
11
11
|
// src/cli/commands/init.ts
|
|
12
12
|
import fs from "fs-extra";
|
|
@@ -49,14 +49,22 @@ function showWelcome() {
|
|
|
49
49
|
console.log("");
|
|
50
50
|
console.log(` ${chalk.cyan("edtools init")} ${chalk.gray("Initialize your project")}`);
|
|
51
51
|
console.log(` ${chalk.cyan("edtools generate")} ${chalk.gray("Generate SEO-optimized posts")}`);
|
|
52
|
+
console.log(` ${chalk.cyan("edtools validate")} ${chalk.gray("Validate post quality")}`);
|
|
52
53
|
console.log(` ${chalk.cyan("edtools analyze")} ${chalk.gray("Analyze Google Search Console CSV")}`);
|
|
53
54
|
console.log("");
|
|
55
|
+
console.log(chalk.bold.cyan(" Advanced Usage:"));
|
|
56
|
+
console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log(` ${chalk.cyan("edtools generate --dry-run")} ${chalk.gray("Preview generation")}`);
|
|
59
|
+
console.log(` ${chalk.cyan("edtools generate --report json")} ${chalk.gray("JSON output")}`);
|
|
60
|
+
console.log(` ${chalk.cyan("edtools validate --threshold 85")} ${chalk.gray("Show posts < 85")}`);
|
|
61
|
+
console.log("");
|
|
54
62
|
console.log(chalk.gray(" Run ") + chalk.cyan("edtools --help") + chalk.gray(" for all commands"));
|
|
55
63
|
console.log("");
|
|
56
64
|
}
|
|
57
65
|
function getVersion() {
|
|
58
66
|
try {
|
|
59
|
-
return "0.
|
|
67
|
+
return "0.6.0";
|
|
60
68
|
} catch {
|
|
61
69
|
return "dev";
|
|
62
70
|
}
|
|
@@ -395,52 +403,120 @@ async function generateCommand(options) {
|
|
|
395
403
|
avoidDuplicates: true,
|
|
396
404
|
similarityThreshold: 0.85,
|
|
397
405
|
provider,
|
|
398
|
-
apiKey
|
|
406
|
+
apiKey,
|
|
407
|
+
dryRun: options.dryRun
|
|
399
408
|
};
|
|
400
409
|
const providerName = provider === "anthropic" ? "Claude" : "ChatGPT";
|
|
401
410
|
const spinner = ora2(`Generating content with ${providerName}...`).start();
|
|
402
411
|
try {
|
|
403
412
|
const result = await generator.generate(generateConfig);
|
|
404
413
|
if (result.success && result.posts.length > 0) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const table = new Table({
|
|
408
|
-
head: [
|
|
409
|
-
chalk3.cyan.bold("#"),
|
|
410
|
-
chalk3.cyan.bold("Title"),
|
|
411
|
-
chalk3.cyan.bold("SEO Score"),
|
|
412
|
-
chalk3.cyan.bold("Path")
|
|
413
|
-
],
|
|
414
|
-
colWidths: [5, 40, 12, 50],
|
|
415
|
-
wordWrap: true,
|
|
416
|
-
style: {
|
|
417
|
-
head: [],
|
|
418
|
-
border: ["cyan"]
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
result.posts.forEach((post, i) => {
|
|
422
|
-
const scoreColor = getScoreColorFn(post.seoScore);
|
|
423
|
-
table.push([
|
|
424
|
-
chalk3.gray((i + 1).toString()),
|
|
425
|
-
chalk3.white(post.title),
|
|
426
|
-
scoreColor(`${post.seoScore}/100`),
|
|
427
|
-
chalk3.gray(post.path)
|
|
428
|
-
]);
|
|
429
|
-
});
|
|
430
|
-
console.log(table.toString());
|
|
414
|
+
const dryRunMode = result.dryRun || false;
|
|
415
|
+
spinner.succeed(chalk3.bold(dryRunMode ? "Preview generated successfully!" : "Content generated successfully!"));
|
|
431
416
|
console.log("");
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const warningText = result.warnings.join("\n");
|
|
435
|
-
console.log(warningBox(warningText));
|
|
417
|
+
if (dryRunMode) {
|
|
418
|
+
console.log(chalk3.yellow.bold("\u{1F50D} DRY RUN MODE - No files were written\n"));
|
|
436
419
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
420
|
+
if (options.report === "json" || options.report === "pretty") {
|
|
421
|
+
const report = {
|
|
422
|
+
success: result.success,
|
|
423
|
+
generated: result.posts.length,
|
|
424
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
425
|
+
files: result.posts.map((post) => ({
|
|
426
|
+
path: post.path,
|
|
427
|
+
slug: post.slug,
|
|
428
|
+
title: post.title,
|
|
429
|
+
url: post.url,
|
|
430
|
+
wordCount: post.wordCount,
|
|
431
|
+
readTime: post.readTime,
|
|
432
|
+
seoScore: post.seoScore,
|
|
433
|
+
issues: post.seoIssues || [],
|
|
434
|
+
metadata: {
|
|
435
|
+
id: post.id,
|
|
436
|
+
tags: post.tags,
|
|
437
|
+
keywords: post.keywords,
|
|
438
|
+
datePublished: post.datePublished
|
|
439
|
+
}
|
|
440
|
+
})),
|
|
441
|
+
manifestPath: result.manifestPath,
|
|
442
|
+
sitemapPath: result.sitemapPath,
|
|
443
|
+
stats: result.stats || {
|
|
444
|
+
avgSeoScore: result.posts.reduce((sum, p) => sum + p.seoScore, 0) / result.posts.length,
|
|
445
|
+
totalWords: result.posts.reduce((sum, p) => sum + p.wordCount, 0),
|
|
446
|
+
avgReadTime: result.posts[0]?.readTime || "0 min"
|
|
447
|
+
},
|
|
448
|
+
warnings: result.warnings || [],
|
|
449
|
+
errors: result.errors || [],
|
|
450
|
+
dryRun: dryRunMode
|
|
451
|
+
};
|
|
452
|
+
const indent = options.report === "pretty" ? 2 : 0;
|
|
453
|
+
console.log(JSON.stringify(report, null, indent));
|
|
454
|
+
} else {
|
|
455
|
+
const table = new Table({
|
|
456
|
+
head: [
|
|
457
|
+
chalk3.cyan.bold("#"),
|
|
458
|
+
chalk3.cyan.bold("Title"),
|
|
459
|
+
chalk3.cyan.bold("Words"),
|
|
460
|
+
chalk3.cyan.bold("SEO"),
|
|
461
|
+
dryRunMode ? chalk3.cyan.bold("Would Create") : chalk3.cyan.bold("Path")
|
|
462
|
+
],
|
|
463
|
+
colWidths: [5, 35, 10, 10, 45],
|
|
464
|
+
wordWrap: true,
|
|
465
|
+
style: {
|
|
466
|
+
head: [],
|
|
467
|
+
border: ["cyan"]
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
result.posts.forEach((post, i) => {
|
|
471
|
+
const scoreColor = getScoreColorFn(post.seoScore);
|
|
472
|
+
table.push([
|
|
473
|
+
chalk3.gray((i + 1).toString()),
|
|
474
|
+
chalk3.white(truncateTitle(post.title, 33)),
|
|
475
|
+
chalk3.white(post.wordCount.toString()),
|
|
476
|
+
scoreColor(`${post.seoScore}/100`),
|
|
477
|
+
chalk3.gray(post.path)
|
|
478
|
+
]);
|
|
479
|
+
});
|
|
480
|
+
console.log(table.toString());
|
|
481
|
+
console.log("");
|
|
482
|
+
if (result.stats) {
|
|
483
|
+
console.log(chalk3.cyan.bold("Statistics:\n"));
|
|
484
|
+
console.log(` Total words: ${chalk3.white(result.stats.totalWords)}`);
|
|
485
|
+
console.log(` Avg SEO score: ${getScoreColorFn(result.stats.avgSeoScore)(result.stats.avgSeoScore + "/100")}`);
|
|
486
|
+
console.log(` Avg read time: ${chalk3.white(result.stats.avgReadTime)}`);
|
|
487
|
+
console.log("");
|
|
488
|
+
}
|
|
489
|
+
if (dryRunMode) {
|
|
490
|
+
console.log(chalk3.yellow.bold("Would create:\n"));
|
|
491
|
+
if (result.manifestPath) {
|
|
492
|
+
console.log(` ${chalk3.yellow("\u2022")} ${chalk3.white(result.manifestPath)} (manifest with ${result.posts.length} posts)`);
|
|
493
|
+
}
|
|
494
|
+
if (result.sitemapPath) {
|
|
495
|
+
console.log(` ${chalk3.yellow("\u2022")} ${chalk3.white(result.sitemapPath)} (sitemap with ${result.posts.length} URLs)`);
|
|
496
|
+
}
|
|
497
|
+
console.log("");
|
|
498
|
+
}
|
|
499
|
+
if (!dryRunMode) {
|
|
500
|
+
console.log(successBox(`Generated ${result.posts.length} SEO-optimized ${result.posts.length === 1 ? "post" : "posts"}!`));
|
|
501
|
+
}
|
|
502
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
503
|
+
const warningText = result.warnings.join("\n");
|
|
504
|
+
console.log(warningBox(warningText));
|
|
505
|
+
}
|
|
506
|
+
if (dryRunMode) {
|
|
507
|
+
console.log(chalk3.cyan.bold("Next step:"));
|
|
508
|
+
console.log(` ${chalk3.cyan("\u2022")} Run without ${chalk3.white("--dry-run")} to generate files
|
|
509
|
+
`);
|
|
510
|
+
} else {
|
|
511
|
+
console.log(chalk3.cyan.bold("Next steps:"));
|
|
512
|
+
console.log(` ${chalk3.cyan("1.")} Review generated content in ${chalk3.white(outputDir)}`);
|
|
513
|
+
console.log(` ${chalk3.cyan("2.")} Edit posts to add personal experience/expertise`);
|
|
514
|
+
console.log(` ${chalk3.cyan("3.")} Deploy to your website`);
|
|
515
|
+
console.log("");
|
|
516
|
+
console.log(chalk3.yellow(`\u{1F4A1} Tip: Wait 3-7 days before generating more posts to avoid spam detection
|
|
443
517
|
`));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
444
520
|
} else {
|
|
445
521
|
spinner.fail(chalk3.bold("Failed to generate content"));
|
|
446
522
|
if (result.errors && result.errors.length > 0) {
|
|
@@ -456,10 +532,15 @@ async function generateCommand(options) {
|
|
|
456
532
|
}
|
|
457
533
|
}
|
|
458
534
|
function getScoreColorFn(score) {
|
|
459
|
-
if (score >=
|
|
460
|
-
if (score >=
|
|
535
|
+
if (score >= 90) return chalk3.green;
|
|
536
|
+
if (score >= 75) return chalk3.yellow;
|
|
537
|
+
if (score >= 60) return chalk3.hex("#FFA500");
|
|
461
538
|
return chalk3.red;
|
|
462
539
|
}
|
|
540
|
+
function truncateTitle(title, maxLen) {
|
|
541
|
+
if (title.length <= maxLen) return title;
|
|
542
|
+
return title.substring(0, maxLen - 3) + "...";
|
|
543
|
+
}
|
|
463
544
|
|
|
464
545
|
// src/cli/commands/analyze.ts
|
|
465
546
|
import fs4 from "fs-extra";
|
|
@@ -720,17 +801,424 @@ Error: ${error.message}
|
|
|
720
801
|
console.log(chalk4.gray("\u{1F4A1} Use --from-csv to automatically generate posts for top opportunities\n"));
|
|
721
802
|
}
|
|
722
803
|
|
|
804
|
+
// src/cli/commands/validate.ts
|
|
805
|
+
import fs6 from "fs-extra";
|
|
806
|
+
import path4 from "path";
|
|
807
|
+
import chalk5 from "chalk";
|
|
808
|
+
import Table3 from "cli-table3";
|
|
809
|
+
|
|
810
|
+
// src/utils/seo-validator.ts
|
|
811
|
+
import * as cheerio from "cheerio";
|
|
812
|
+
import fs5 from "fs-extra";
|
|
813
|
+
async function validatePost(htmlPath) {
|
|
814
|
+
const html = await fs5.readFile(htmlPath, "utf-8");
|
|
815
|
+
const $ = cheerio.load(html);
|
|
816
|
+
const issues = [];
|
|
817
|
+
const passed = [];
|
|
818
|
+
const metadata = {};
|
|
819
|
+
const title = $("title").text().trim();
|
|
820
|
+
metadata.titleLength = title.length;
|
|
821
|
+
if (!title) {
|
|
822
|
+
issues.push({
|
|
823
|
+
severity: "error",
|
|
824
|
+
category: "metadata",
|
|
825
|
+
message: "Missing <title> tag",
|
|
826
|
+
suggestion: "Add a descriptive title (50-60 characters)"
|
|
827
|
+
});
|
|
828
|
+
} else if (title.length < 30) {
|
|
829
|
+
issues.push({
|
|
830
|
+
severity: "warning",
|
|
831
|
+
category: "metadata",
|
|
832
|
+
message: `Title too short (${title.length} chars)`,
|
|
833
|
+
suggestion: "Optimal length is 50-60 characters for better CTR"
|
|
834
|
+
});
|
|
835
|
+
} else if (title.length > 60) {
|
|
836
|
+
issues.push({
|
|
837
|
+
severity: "warning",
|
|
838
|
+
category: "metadata",
|
|
839
|
+
message: `Title too long (${title.length} chars)`,
|
|
840
|
+
suggestion: "Shorten to 50-60 characters to avoid truncation in SERPs"
|
|
841
|
+
});
|
|
842
|
+
} else {
|
|
843
|
+
passed.push(`Title length optimal (${title.length} chars)`);
|
|
844
|
+
}
|
|
845
|
+
const metaDesc = $('meta[name="description"]').attr("content")?.trim() || "";
|
|
846
|
+
metadata.descriptionLength = metaDesc.length;
|
|
847
|
+
if (!metaDesc) {
|
|
848
|
+
issues.push({
|
|
849
|
+
severity: "error",
|
|
850
|
+
category: "metadata",
|
|
851
|
+
message: "Missing meta description",
|
|
852
|
+
suggestion: "Add a compelling description (150-160 characters)"
|
|
853
|
+
});
|
|
854
|
+
} else if (metaDesc.length < 120) {
|
|
855
|
+
issues.push({
|
|
856
|
+
severity: "warning",
|
|
857
|
+
category: "metadata",
|
|
858
|
+
message: `Meta description too short (${metaDesc.length}/160 chars)`,
|
|
859
|
+
suggestion: `Add ${160 - metaDesc.length} more characters to optimize CTR`
|
|
860
|
+
});
|
|
861
|
+
} else if (metaDesc.length > 160) {
|
|
862
|
+
issues.push({
|
|
863
|
+
severity: "warning",
|
|
864
|
+
category: "metadata",
|
|
865
|
+
message: `Meta description too long (${metaDesc.length}/160 chars)`,
|
|
866
|
+
suggestion: `Remove ${metaDesc.length - 160} characters to avoid truncation`
|
|
867
|
+
});
|
|
868
|
+
} else {
|
|
869
|
+
passed.push(`Meta description optimal (${metaDesc.length} chars)`);
|
|
870
|
+
}
|
|
871
|
+
const h1Elements = $("h1");
|
|
872
|
+
const h1Count = h1Elements.length;
|
|
873
|
+
metadata.h1Count = h1Count;
|
|
874
|
+
if (h1Count === 0) {
|
|
875
|
+
issues.push({
|
|
876
|
+
severity: "error",
|
|
877
|
+
category: "headings",
|
|
878
|
+
message: "Missing H1 heading",
|
|
879
|
+
suggestion: "Add a single H1 with primary keyword"
|
|
880
|
+
});
|
|
881
|
+
} else if (h1Count > 1) {
|
|
882
|
+
issues.push({
|
|
883
|
+
severity: "warning",
|
|
884
|
+
category: "headings",
|
|
885
|
+
message: `Multiple H1 tags found (${h1Count})`,
|
|
886
|
+
suggestion: "Use only one H1 per page for proper structure"
|
|
887
|
+
});
|
|
888
|
+
} else {
|
|
889
|
+
passed.push("Single H1 heading");
|
|
890
|
+
}
|
|
891
|
+
const h2Count = $("h2").length;
|
|
892
|
+
metadata.h2Count = h2Count;
|
|
893
|
+
if (h2Count < 2) {
|
|
894
|
+
issues.push({
|
|
895
|
+
severity: "warning",
|
|
896
|
+
category: "headings",
|
|
897
|
+
message: `Few H2 headings for readability (${h2Count} found)`,
|
|
898
|
+
suggestion: "Add 3-5 H2 headings for better structure and scannability"
|
|
899
|
+
});
|
|
900
|
+
} else if (h2Count > 10) {
|
|
901
|
+
issues.push({
|
|
902
|
+
severity: "info",
|
|
903
|
+
category: "headings",
|
|
904
|
+
message: `Many H2 headings (${h2Count})`,
|
|
905
|
+
suggestion: "Consider if all sections need H2 or some should be H3"
|
|
906
|
+
});
|
|
907
|
+
} else {
|
|
908
|
+
passed.push(`Good heading structure (${h2Count} H2s)`);
|
|
909
|
+
}
|
|
910
|
+
const images = $("img");
|
|
911
|
+
metadata.imageCount = images.length;
|
|
912
|
+
const imagesWithoutAlt = images.filter((i, el) => !$(el).attr("alt")).length;
|
|
913
|
+
if (imagesWithoutAlt > 0) {
|
|
914
|
+
issues.push({
|
|
915
|
+
severity: "warning",
|
|
916
|
+
category: "images",
|
|
917
|
+
message: `${imagesWithoutAlt} image${imagesWithoutAlt > 1 ? "s" : ""} missing alt text`,
|
|
918
|
+
suggestion: "Add descriptive alt text to all images for accessibility and SEO"
|
|
919
|
+
});
|
|
920
|
+
} else if (images.length > 0) {
|
|
921
|
+
passed.push(`All images have alt text (${images.length} images)`);
|
|
922
|
+
}
|
|
923
|
+
const schemaScript = $('script[type="application/ld+json"]');
|
|
924
|
+
if (schemaScript.length === 0) {
|
|
925
|
+
issues.push({
|
|
926
|
+
severity: "info",
|
|
927
|
+
category: "structure",
|
|
928
|
+
message: "Missing Schema.org markup",
|
|
929
|
+
suggestion: "Add structured data for rich snippets in search results"
|
|
930
|
+
});
|
|
931
|
+
} else {
|
|
932
|
+
passed.push("Has Schema.org markup");
|
|
933
|
+
}
|
|
934
|
+
const metaKeywords = $('meta[name="keywords"]');
|
|
935
|
+
if (metaKeywords.length > 0) {
|
|
936
|
+
issues.push({
|
|
937
|
+
severity: "info",
|
|
938
|
+
category: "metadata",
|
|
939
|
+
message: "Using deprecated meta keywords tag",
|
|
940
|
+
suggestion: "Remove meta keywords tag (not used by modern search engines)"
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const ogTitle = $('meta[property="og:title"]').attr("content");
|
|
944
|
+
const ogDescription = $('meta[property="og:description"]').attr("content");
|
|
945
|
+
const ogImage = $('meta[property="og:image"]').attr("content");
|
|
946
|
+
if (!ogTitle || !ogDescription) {
|
|
947
|
+
issues.push({
|
|
948
|
+
severity: "info",
|
|
949
|
+
category: "metadata",
|
|
950
|
+
message: "Missing Open Graph tags",
|
|
951
|
+
suggestion: "Add og:title, og:description, og:image for better social sharing"
|
|
952
|
+
});
|
|
953
|
+
} else {
|
|
954
|
+
passed.push("Has Open Graph tags");
|
|
955
|
+
}
|
|
956
|
+
const bodyText = $("article, main, body").text();
|
|
957
|
+
const wordCount = bodyText.replace(/\s+/g, " ").trim().split(/\s+/).filter((word) => word.length > 0).length;
|
|
958
|
+
metadata.wordCount = wordCount;
|
|
959
|
+
if (wordCount < 300) {
|
|
960
|
+
issues.push({
|
|
961
|
+
severity: "warning",
|
|
962
|
+
category: "content",
|
|
963
|
+
message: `Content too short (${wordCount} words)`,
|
|
964
|
+
suggestion: "Aim for at least 1000 words for better SEO performance"
|
|
965
|
+
});
|
|
966
|
+
} else if (wordCount < 1e3) {
|
|
967
|
+
issues.push({
|
|
968
|
+
severity: "info",
|
|
969
|
+
category: "content",
|
|
970
|
+
message: `Content could be longer (${wordCount} words)`,
|
|
971
|
+
suggestion: "Consider expanding to 1500+ words for comprehensive coverage"
|
|
972
|
+
});
|
|
973
|
+
} else {
|
|
974
|
+
passed.push(`Good content length (${wordCount} words)`);
|
|
975
|
+
}
|
|
976
|
+
const links = $("a[href]");
|
|
977
|
+
const internalLinks = links.filter((i, el) => {
|
|
978
|
+
const href = $(el).attr("href") || "";
|
|
979
|
+
return href.startsWith("/") || href.startsWith("#");
|
|
980
|
+
}).length;
|
|
981
|
+
const externalLinks = links.length - internalLinks;
|
|
982
|
+
if (internalLinks === 0 && links.length > 0) {
|
|
983
|
+
issues.push({
|
|
984
|
+
severity: "info",
|
|
985
|
+
category: "links",
|
|
986
|
+
message: "No internal links found",
|
|
987
|
+
suggestion: "Add 2-3 internal links to related content"
|
|
988
|
+
});
|
|
989
|
+
} else if (internalLinks > 0) {
|
|
990
|
+
passed.push(`Has internal links (${internalLinks} links)`);
|
|
991
|
+
}
|
|
992
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
993
|
+
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
994
|
+
const infoCount = issues.filter((i) => i.severity === "info").length;
|
|
995
|
+
let score = 100;
|
|
996
|
+
score -= errorCount * 20;
|
|
997
|
+
score -= warningCount * 7;
|
|
998
|
+
score -= infoCount * 2;
|
|
999
|
+
score = Math.max(0, Math.min(100, score));
|
|
1000
|
+
return {
|
|
1001
|
+
path: htmlPath,
|
|
1002
|
+
title: title || "Untitled",
|
|
1003
|
+
seoScore: score,
|
|
1004
|
+
issues,
|
|
1005
|
+
passed,
|
|
1006
|
+
metadata
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function calculateValidationStats(results) {
|
|
1010
|
+
const totalPosts = results.length;
|
|
1011
|
+
const avgSeoScore = results.reduce((sum, r) => sum + r.seoScore, 0) / totalPosts;
|
|
1012
|
+
const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
|
|
1013
|
+
const postsWithIssues = results.filter((r) => r.issues.length > 0).length;
|
|
1014
|
+
const issuesBySeverity = {
|
|
1015
|
+
error: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "error").length, 0),
|
|
1016
|
+
warning: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "warning").length, 0),
|
|
1017
|
+
info: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "info").length, 0)
|
|
1018
|
+
};
|
|
1019
|
+
const issuesByCategory = results.reduce((acc, r) => {
|
|
1020
|
+
r.issues.forEach((issue) => {
|
|
1021
|
+
acc[issue.category] = (acc[issue.category] || 0) + 1;
|
|
1022
|
+
});
|
|
1023
|
+
return acc;
|
|
1024
|
+
}, {});
|
|
1025
|
+
return {
|
|
1026
|
+
totalPosts,
|
|
1027
|
+
avgSeoScore: Math.round(avgSeoScore * 10) / 10,
|
|
1028
|
+
totalIssues,
|
|
1029
|
+
postsWithIssues,
|
|
1030
|
+
issuesBySeverity,
|
|
1031
|
+
issuesByCategory
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/cli/commands/validate.ts
|
|
1036
|
+
async function validateCommand(options) {
|
|
1037
|
+
console.log(chalk5.cyan.bold("\n\u{1F4CA} Validating SEO quality...\n"));
|
|
1038
|
+
const projectPath = process.cwd();
|
|
1039
|
+
let htmlFiles = [];
|
|
1040
|
+
if (options.post) {
|
|
1041
|
+
const postPath = path4.resolve(projectPath, options.post);
|
|
1042
|
+
if (!await fs6.pathExists(postPath)) {
|
|
1043
|
+
console.log(errorBox(`Post not found: ${postPath}`));
|
|
1044
|
+
process.exit(1);
|
|
1045
|
+
}
|
|
1046
|
+
htmlFiles = [postPath];
|
|
1047
|
+
} else if (options.posts) {
|
|
1048
|
+
const postsDir = path4.resolve(projectPath, options.posts);
|
|
1049
|
+
if (!await fs6.pathExists(postsDir)) {
|
|
1050
|
+
console.log(errorBox(`Directory not found: ${postsDir}`));
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
htmlFiles = await findHtmlFiles(postsDir);
|
|
1054
|
+
if (htmlFiles.length === 0) {
|
|
1055
|
+
console.log(errorBox(`No HTML files found in: ${postsDir}`));
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
} else {
|
|
1059
|
+
const defaultBlogDir = path4.join(projectPath, "blog");
|
|
1060
|
+
if (await fs6.pathExists(defaultBlogDir)) {
|
|
1061
|
+
htmlFiles = await findHtmlFiles(defaultBlogDir);
|
|
1062
|
+
if (htmlFiles.length === 0) {
|
|
1063
|
+
console.log(errorBox("No HTML files found in blog/ directory"));
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
} else {
|
|
1067
|
+
console.log(errorBox("No posts directory specified"));
|
|
1068
|
+
console.log(chalk5.yellow("Usage: edtools validate --posts <dir> or --post <file>\n"));
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
console.log(chalk5.cyan(`Found ${htmlFiles.length} post${htmlFiles.length > 1 ? "s" : ""} to validate
|
|
1073
|
+
`));
|
|
1074
|
+
const results = [];
|
|
1075
|
+
for (const htmlFile of htmlFiles) {
|
|
1076
|
+
try {
|
|
1077
|
+
const result = await validatePost(htmlFile);
|
|
1078
|
+
results.push(result);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
console.log(chalk5.yellow(`\u26A0\uFE0F Failed to validate ${htmlFile}: ${error.message}`));
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (results.length === 0) {
|
|
1084
|
+
console.log(errorBox("No posts could be validated"));
|
|
1085
|
+
process.exit(1);
|
|
1086
|
+
}
|
|
1087
|
+
const threshold = options.threshold ? parseInt(options.threshold, 10) : 0;
|
|
1088
|
+
const filteredResults = threshold > 0 ? results.filter((r) => r.seoScore < threshold) : results;
|
|
1089
|
+
if (options.output) {
|
|
1090
|
+
const stats2 = calculateValidationStats(results);
|
|
1091
|
+
const report = {
|
|
1092
|
+
success: true,
|
|
1093
|
+
validated: results.length,
|
|
1094
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1095
|
+
posts: results.map((r) => ({
|
|
1096
|
+
path: r.path,
|
|
1097
|
+
title: r.title,
|
|
1098
|
+
seoScore: r.seoScore,
|
|
1099
|
+
issues: r.issues,
|
|
1100
|
+
passed: r.passed,
|
|
1101
|
+
metadata: r.metadata
|
|
1102
|
+
})),
|
|
1103
|
+
stats: stats2
|
|
1104
|
+
};
|
|
1105
|
+
const outputPath = path4.resolve(projectPath, options.output);
|
|
1106
|
+
await fs6.writeJson(outputPath, report, { spaces: 2 });
|
|
1107
|
+
console.log(successBox(`Validation report saved to ${outputPath}`));
|
|
1108
|
+
}
|
|
1109
|
+
if (filteredResults.length > 0) {
|
|
1110
|
+
const table = new Table3({
|
|
1111
|
+
head: [
|
|
1112
|
+
chalk5.cyan.bold("#"),
|
|
1113
|
+
chalk5.cyan.bold("Title"),
|
|
1114
|
+
chalk5.cyan.bold("Score"),
|
|
1115
|
+
chalk5.cyan.bold("Issues")
|
|
1116
|
+
],
|
|
1117
|
+
colWidths: [5, 40, 10, 50],
|
|
1118
|
+
wordWrap: true,
|
|
1119
|
+
style: {
|
|
1120
|
+
head: [],
|
|
1121
|
+
border: ["cyan"]
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
filteredResults.forEach((result, i) => {
|
|
1125
|
+
const scoreColor = getScoreColor(result.seoScore);
|
|
1126
|
+
const issuesText = result.issues.length > 0 ? result.issues.map((issue) => {
|
|
1127
|
+
const icon = issue.severity === "error" ? "\u2717" : issue.severity === "warning" ? "\u26A0" : "\u2139";
|
|
1128
|
+
const color = issue.severity === "error" ? chalk5.red : issue.severity === "warning" ? chalk5.yellow : chalk5.blue;
|
|
1129
|
+
return color(`${icon} ${issue.message}`);
|
|
1130
|
+
}).join("\n") : chalk5.green("\u2713 No issues");
|
|
1131
|
+
table.push([
|
|
1132
|
+
chalk5.gray((i + 1).toString()),
|
|
1133
|
+
chalk5.white(truncate(result.title, 38)),
|
|
1134
|
+
scoreColor(`${result.seoScore}/100`),
|
|
1135
|
+
issuesText
|
|
1136
|
+
]);
|
|
1137
|
+
});
|
|
1138
|
+
console.log(table.toString());
|
|
1139
|
+
console.log("");
|
|
1140
|
+
} else if (threshold > 0) {
|
|
1141
|
+
console.log(successBox(`All posts have SEO score >= ${threshold}`));
|
|
1142
|
+
}
|
|
1143
|
+
const stats = calculateValidationStats(results);
|
|
1144
|
+
console.log(chalk5.cyan.bold("Overall Statistics:\n"));
|
|
1145
|
+
console.log(` Total posts validated: ${chalk5.white(stats.totalPosts)}`);
|
|
1146
|
+
console.log(` Average SEO score: ${getScoreColor(stats.avgSeoScore)(stats.avgSeoScore + "/100")}`);
|
|
1147
|
+
console.log(` Posts with issues: ${chalk5.white(stats.postsWithIssues)} (${Math.round(stats.postsWithIssues / stats.totalPosts * 100)}%)`);
|
|
1148
|
+
console.log(` Total issues found: ${chalk5.white(stats.totalIssues)}`);
|
|
1149
|
+
console.log("");
|
|
1150
|
+
if (stats.totalIssues > 0) {
|
|
1151
|
+
console.log(chalk5.cyan.bold("Issues by Severity:\n"));
|
|
1152
|
+
if (stats.issuesBySeverity.error > 0) {
|
|
1153
|
+
console.log(` ${chalk5.red("\u2717 Errors:")} ${chalk5.white(stats.issuesBySeverity.error)}`);
|
|
1154
|
+
}
|
|
1155
|
+
if (stats.issuesBySeverity.warning > 0) {
|
|
1156
|
+
console.log(` ${chalk5.yellow("\u26A0 Warnings:")} ${chalk5.white(stats.issuesBySeverity.warning)}`);
|
|
1157
|
+
}
|
|
1158
|
+
if (stats.issuesBySeverity.info > 0) {
|
|
1159
|
+
console.log(` ${chalk5.blue("\u2139 Info:")} ${chalk5.white(stats.issuesBySeverity.info)}`);
|
|
1160
|
+
}
|
|
1161
|
+
console.log("");
|
|
1162
|
+
}
|
|
1163
|
+
if (Object.keys(stats.issuesByCategory).length > 0) {
|
|
1164
|
+
console.log(chalk5.cyan.bold("Top Issue Categories:\n"));
|
|
1165
|
+
const sortedCategories = Object.entries(stats.issuesByCategory).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
1166
|
+
sortedCategories.forEach(([category, count]) => {
|
|
1167
|
+
console.log(` ${chalk5.white(capitalize(category))}: ${chalk5.yellow(count)}`);
|
|
1168
|
+
});
|
|
1169
|
+
console.log("");
|
|
1170
|
+
}
|
|
1171
|
+
if (stats.avgSeoScore < 85) {
|
|
1172
|
+
console.log(chalk5.yellow.bold("\u{1F4A1} Recommendations:\n"));
|
|
1173
|
+
console.log(chalk5.yellow(" \u2022 Focus on fixing critical errors first"));
|
|
1174
|
+
console.log(chalk5.yellow(" \u2022 Optimize meta descriptions to 150-160 characters"));
|
|
1175
|
+
console.log(chalk5.yellow(" \u2022 Ensure all posts have proper heading structure (H1, H2s)"));
|
|
1176
|
+
console.log(chalk5.yellow(" \u2022 Add alt text to all images"));
|
|
1177
|
+
console.log("");
|
|
1178
|
+
} else {
|
|
1179
|
+
console.log(successBox(`Great job! Average SEO score is ${stats.avgSeoScore}/100`));
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async function findHtmlFiles(dir) {
|
|
1183
|
+
const files = [];
|
|
1184
|
+
const entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1185
|
+
for (const entry of entries) {
|
|
1186
|
+
const fullPath = path4.join(dir, entry.name);
|
|
1187
|
+
if (entry.isDirectory()) {
|
|
1188
|
+
const subFiles = await findHtmlFiles(fullPath);
|
|
1189
|
+
files.push(...subFiles);
|
|
1190
|
+
} else if (entry.isFile() && entry.name.endsWith(".html")) {
|
|
1191
|
+
files.push(fullPath);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return files;
|
|
1195
|
+
}
|
|
1196
|
+
function getScoreColor(score) {
|
|
1197
|
+
if (score >= 90) return chalk5.green;
|
|
1198
|
+
if (score >= 75) return chalk5.yellow;
|
|
1199
|
+
if (score >= 60) return chalk5.hex("#FFA500");
|
|
1200
|
+
return chalk5.red;
|
|
1201
|
+
}
|
|
1202
|
+
function truncate(str, maxLen) {
|
|
1203
|
+
if (str.length <= maxLen) return str;
|
|
1204
|
+
return str.substring(0, maxLen - 3) + "...";
|
|
1205
|
+
}
|
|
1206
|
+
function capitalize(str) {
|
|
1207
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
723
1210
|
// src/cli/index.ts
|
|
724
1211
|
var program = new Command();
|
|
725
|
-
program.name("edtools").description("AI-Powered Content Marketing CLI").version("0.
|
|
1212
|
+
program.name("edtools").description("AI-Powered Content Marketing CLI - Generate, validate, and optimize SEO content").version("0.6.0");
|
|
726
1213
|
program.command("init").description("Initialize edtools in your project").option("-p, --path <path>", "Project path", process.cwd()).action(initCommand);
|
|
727
|
-
program.command("generate").description("Generate blog posts").option("-n, --posts <number>", "Number of posts to generate", "3").option("-t, --topics <topics...>", "Specific topics to write about").option("-o, --output <dir>", "Output directory", "./blog").option("--api-key <key>", "
|
|
1214
|
+
program.command("generate").description("Generate SEO-optimized blog posts").option("-n, --posts <number>", "Number of posts to generate (1-10)", "3").option("-t, --topics <topics...>", "Specific topics to write about").option("-o, --output <dir>", "Output directory", "./blog").option("--api-key <key>", "API key (or use ANTHROPIC_API_KEY/OPENAI_API_KEY env var)").option("--from-csv", "Generate from CSV analysis opportunities").option("--dry-run", "Preview what would be generated without writing files").option("--report <format>", "Output format: json, table, pretty (default: table)", "table").action(generateCommand);
|
|
728
1215
|
program.command("analyze").description("Analyze Google Search Console CSV data").option("-f, --file <path>", "Path to CSV file (auto-detects if not provided)").option("--min-impressions <number>", "Minimum impressions for opportunities", "50").option("--min-position <number>", "Minimum position for opportunities", "20").option("--limit <number>", "Number of opportunities to show", "10").action(analyzeCommand);
|
|
1216
|
+
program.command("validate").description("Validate SEO quality of existing posts").option("--posts <dir>", "Posts directory to validate").option("--post <file>", "Single post file to validate").option("--output <file>", "Save validation report as JSON").option("--threshold <score>", "Only show posts below this score").action(validateCommand);
|
|
729
1217
|
program.command("config").description("View or set configuration").option("--set-api-key <key>", "Set Anthropic API key").action(async (options) => {
|
|
730
1218
|
if (options.setApiKey) {
|
|
731
|
-
console.log(
|
|
1219
|
+
console.log(chalk6.green("\u2713 API key saved"));
|
|
732
1220
|
} else {
|
|
733
|
-
console.log(
|
|
1221
|
+
console.log(chalk6.cyan("Configuration:"));
|
|
734
1222
|
console.log(` API Key: ${process.env.ANTHROPIC_API_KEY ? "[set]" : "[not set]"}`);
|
|
735
1223
|
}
|
|
736
1224
|
});
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/core/generator.ts"],"names":[],"mappings":"AAWA,OAAO,EAKL,cAAc,EACd,cAAc,EAEd,UAAU,EAGX,MAAM,mBAAmB,CAAC;AAW3B,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,QAAQ,CAAkB;gBAEtB,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAE,UAAwB;IAuBzD,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/core/generator.ts"],"names":[],"mappings":"AAWA,OAAO,EAKL,cAAc,EACd,cAAc,EAEd,UAAU,EAGX,MAAM,mBAAmB,CAAC;AAW3B,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,QAAQ,CAAkB;gBAEtB,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAE,UAAwB;IAuBzD,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;YAiHjD,eAAe;YAsEf,qBAAqB;YA+BrB,kBAAkB;IA8BhC,OAAO,CAAC,WAAW;IAkDnB,OAAO,CAAC,iBAAiB;YAwBX,iBAAiB;YAsCjB,cAAc;YAqEd,gBAAgB;YA4DhB,iBAAiB;YA8CjB,aAAa;CAkC5B"}
|