@edtools/cli 0.4.1 → 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.
Files changed (42) hide show
  1. package/dist/adapters/html/index.d.ts.map +1 -1
  2. package/dist/adapters/html/index.js +14 -3
  3. package/dist/adapters/html/index.js.map +1 -1
  4. package/dist/adapters/html/templates/blog-index.html.ejs +342 -0
  5. package/dist/adapters/html/templates/blog-post.html.ejs +48 -0
  6. package/dist/chunk-AJSCN4WK.js +663 -0
  7. package/dist/chunk-INVECVSW.js +45 -0
  8. package/dist/chunk-JCUQ7D56.js +688 -0
  9. package/dist/chunk-UMREI3K7.js +652 -0
  10. package/dist/chunk-ZDV7BJZ4.js +652 -0
  11. package/dist/chunk-ZG6JJCRI.js +539 -0
  12. package/dist/cli/commands/generate.d.ts +2 -0
  13. package/dist/cli/commands/generate.d.ts.map +1 -1
  14. package/dist/cli/commands/generate.js +114 -38
  15. package/dist/cli/commands/generate.js.map +1 -1
  16. package/dist/cli/commands/validate.d.ts +9 -0
  17. package/dist/cli/commands/validate.d.ts.map +1 -0
  18. package/dist/cli/commands/validate.js +196 -0
  19. package/dist/cli/commands/validate.js.map +1 -0
  20. package/dist/cli/index.js +535 -46
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/content-helpers-TXVEJMQK.js +16 -0
  23. package/dist/core/generator.d.ts +2 -0
  24. package/dist/core/generator.d.ts.map +1 -1
  25. package/dist/core/generator.js +148 -10
  26. package/dist/core/generator.js.map +1 -1
  27. package/dist/index.d.ts +92 -1
  28. package/dist/index.js +2 -1
  29. package/dist/types/content.d.ts +89 -0
  30. package/dist/types/content.d.ts.map +1 -1
  31. package/dist/ui/banner.d.ts.map +1 -1
  32. package/dist/ui/banner.js +9 -1
  33. package/dist/ui/banner.js.map +1 -1
  34. package/dist/utils/content-helpers.d.ts +7 -0
  35. package/dist/utils/content-helpers.d.ts.map +1 -0
  36. package/dist/utils/content-helpers.js +48 -0
  37. package/dist/utils/content-helpers.js.map +1 -0
  38. package/dist/utils/seo-validator.d.ts +35 -0
  39. package/dist/utils/seo-validator.d.ts.map +1 -0
  40. package/dist/utils/seo-validator.js +244 -0
  41. package/dist/utils/seo-validator.js.map +1 -0
  42. package/package.json +3 -3
package/dist/cli/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ContentGenerator
4
- } from "../chunk-QGUKVOMJ.js";
4
+ } from "../chunk-JCUQ7D56.js";
5
+ import "../chunk-INVECVSW.js";
5
6
 
6
7
  // src/cli/index.ts
7
8
  import { Command } from "commander";
8
- import chalk5 from "chalk";
9
+ import chalk6 from "chalk";
9
10
 
10
11
  // src/cli/commands/init.ts
11
12
  import fs from "fs-extra";
@@ -48,14 +49,22 @@ function showWelcome() {
48
49
  console.log("");
49
50
  console.log(` ${chalk.cyan("edtools init")} ${chalk.gray("Initialize your project")}`);
50
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")}`);
51
53
  console.log(` ${chalk.cyan("edtools analyze")} ${chalk.gray("Analyze Google Search Console CSV")}`);
52
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("");
53
62
  console.log(chalk.gray(" Run ") + chalk.cyan("edtools --help") + chalk.gray(" for all commands"));
54
63
  console.log("");
55
64
  }
56
65
  function getVersion() {
57
66
  try {
58
- return "0.4.1";
67
+ return "0.6.0";
59
68
  } catch {
60
69
  return "dev";
61
70
  }
@@ -394,52 +403,120 @@ async function generateCommand(options) {
394
403
  avoidDuplicates: true,
395
404
  similarityThreshold: 0.85,
396
405
  provider,
397
- apiKey
406
+ apiKey,
407
+ dryRun: options.dryRun
398
408
  };
399
409
  const providerName = provider === "anthropic" ? "Claude" : "ChatGPT";
400
410
  const spinner = ora2(`Generating content with ${providerName}...`).start();
401
411
  try {
402
412
  const result = await generator.generate(generateConfig);
403
413
  if (result.success && result.posts.length > 0) {
404
- spinner.succeed(chalk3.bold("Content generated successfully!"));
405
- console.log("");
406
- const table = new Table({
407
- head: [
408
- chalk3.cyan.bold("#"),
409
- chalk3.cyan.bold("Title"),
410
- chalk3.cyan.bold("SEO Score"),
411
- chalk3.cyan.bold("Path")
412
- ],
413
- colWidths: [5, 40, 12, 50],
414
- wordWrap: true,
415
- style: {
416
- head: [],
417
- border: ["cyan"]
418
- }
419
- });
420
- result.posts.forEach((post, i) => {
421
- const scoreColor = getScoreColorFn(post.seoScore);
422
- table.push([
423
- chalk3.gray((i + 1).toString()),
424
- chalk3.white(post.title),
425
- scoreColor(`${post.seoScore}/100`),
426
- chalk3.gray(post.path)
427
- ]);
428
- });
429
- console.log(table.toString());
414
+ const dryRunMode = result.dryRun || false;
415
+ spinner.succeed(chalk3.bold(dryRunMode ? "Preview generated successfully!" : "Content generated successfully!"));
430
416
  console.log("");
431
- console.log(successBox(`Generated ${result.posts.length} SEO-optimized ${result.posts.length === 1 ? "post" : "posts"}!`));
432
- if (result.warnings && result.warnings.length > 0) {
433
- const warningText = result.warnings.join("\n");
434
- console.log(warningBox(warningText));
417
+ if (dryRunMode) {
418
+ console.log(chalk3.yellow.bold("\u{1F50D} DRY RUN MODE - No files were written\n"));
435
419
  }
436
- console.log(chalk3.cyan.bold("Next steps:"));
437
- console.log(` ${chalk3.cyan("1.")} Review generated content in ${chalk3.white(outputDir)}`);
438
- console.log(` ${chalk3.cyan("2.")} Edit posts to add personal experience/expertise`);
439
- console.log(` ${chalk3.cyan("3.")} Deploy to your website`);
440
- console.log("");
441
- console.log(chalk3.yellow(`\u{1F4A1} Tip: Wait 3-7 days before generating more posts to avoid spam detection
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
442
517
  `));
518
+ }
519
+ }
443
520
  } else {
444
521
  spinner.fail(chalk3.bold("Failed to generate content"));
445
522
  if (result.errors && result.errors.length > 0) {
@@ -455,10 +532,15 @@ async function generateCommand(options) {
455
532
  }
456
533
  }
457
534
  function getScoreColorFn(score) {
458
- if (score >= 80) return chalk3.green;
459
- if (score >= 60) return chalk3.yellow;
535
+ if (score >= 90) return chalk3.green;
536
+ if (score >= 75) return chalk3.yellow;
537
+ if (score >= 60) return chalk3.hex("#FFA500");
460
538
  return chalk3.red;
461
539
  }
540
+ function truncateTitle(title, maxLen) {
541
+ if (title.length <= maxLen) return title;
542
+ return title.substring(0, maxLen - 3) + "...";
543
+ }
462
544
 
463
545
  // src/cli/commands/analyze.ts
464
546
  import fs4 from "fs-extra";
@@ -719,17 +801,424 @@ Error: ${error.message}
719
801
  console.log(chalk4.gray("\u{1F4A1} Use --from-csv to automatically generate posts for top opportunities\n"));
720
802
  }
721
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
+
722
1210
  // src/cli/index.ts
723
1211
  var program = new Command();
724
- program.name("edtools").description("AI-Powered Content Marketing CLI").version("0.4.1");
1212
+ program.name("edtools").description("AI-Powered Content Marketing CLI - Generate, validate, and optimize SEO content").version("0.6.0");
725
1213
  program.command("init").description("Initialize edtools in your project").option("-p, --path <path>", "Project path", process.cwd()).action(initCommand);
726
- 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>", "Anthropic API key (or set ANTHROPIC_API_KEY env var)").option("--from-csv", "Generate from CSV analysis opportunities").action(generateCommand);
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);
727
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);
728
1217
  program.command("config").description("View or set configuration").option("--set-api-key <key>", "Set Anthropic API key").action(async (options) => {
729
1218
  if (options.setApiKey) {
730
- console.log(chalk5.green("\u2713 API key saved"));
1219
+ console.log(chalk6.green("\u2713 API key saved"));
731
1220
  } else {
732
- console.log(chalk5.cyan("Configuration:"));
1221
+ console.log(chalk6.cyan("Configuration:"));
733
1222
  console.log(` API Key: ${process.env.ANTHROPIC_API_KEY ? "[set]" : "[not set]"}`);
734
1223
  }
735
1224
  });
@@ -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,kCAAkC,CAAC;KAC/C,OAAO,CAAC,OAAO,CAAC,CAAC;AAGpB,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,oCAAoC,CAAC;KACjD,MAAM,CAAC,mBAAmB,EAAE,cAAc,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;KAC1D,MAAM,CAAC,WAAW,CAAC,CAAC;AAGvB,OAAO;KACJ,OAAO,CAAC,UAAU,CAAC;KACnB,WAAW,CAAC,qBAAqB,CAAC;KAClC,MAAM,CAAC,sBAAsB,EAAE,6BAA6B,EAAE,GAAG,CAAC;KAClE,MAAM,CAAC,0BAA0B,EAAE,gCAAgC,CAAC;KACpE,MAAM,CAAC,oBAAoB,EAAE,kBAAkB,EAAE,QAAQ,CAAC;KAC1D,MAAM,CAAC,iBAAiB,EAAE,sDAAsD,CAAC;KACjF,MAAM,CAAC,YAAY,EAAE,0CAA0C,CAAC;KAChE,MAAM,CAAC,eAAe,CAAC,CAAC;AAG3B,OAAO;KACJ,OAAO,CAAC,SAAS,CAAC;KAClB,WAAW,CAAC,wCAAwC,CAAC;KACrD,MAAM,CAAC,mBAAmB,EAAE,iDAAiD,CAAC;KAC9E,MAAM,CAAC,4BAA4B,EAAE,uCAAuC,EAAE,IAAI,CAAC;KACnF,MAAM,CAAC,yBAAyB,EAAE,oCAAoC,EAAE,IAAI,CAAC;KAC7E,MAAM,CAAC,kBAAkB,EAAE,iCAAiC,EAAE,IAAI,CAAC;KACnE,MAAM,CAAC,cAAc,CAAC,CAAC;AAG1B,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,2BAA2B,CAAC;KACxC,MAAM,CAAC,qBAAqB,EAAE,uBAAuB,CAAC;KACtD,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QAEtB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC;IAC9C,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,cAAc,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IACrF,CAAC;AACH,CAAC,CAAC,CAAC;AAGL,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC9B,WAAW,EAAE,CAAC;IACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAGD,OAAO,CAAC,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import {
2
+ calculateAvgReadTime,
3
+ calculateReadTime,
4
+ calculateWordCount,
5
+ extractShortDescription,
6
+ generateTimestamp,
7
+ generateUUID
8
+ } from "./chunk-INVECVSW.js";
9
+ export {
10
+ calculateAvgReadTime,
11
+ calculateReadTime,
12
+ calculateWordCount,
13
+ extractShortDescription,
14
+ generateTimestamp,
15
+ generateUUID
16
+ };
@@ -13,6 +13,8 @@ export declare class ContentGenerator {
13
13
  private generateSchemaOrg;
14
14
  private calculateSEOScore;
15
15
  private generateTopics;
16
+ private generateManifest;
17
+ private generateBlogIndex;
16
18
  private updateSitemap;
17
19
  }
18
20
  //# sourceMappingURL=generator.d.ts.map