geo-ai-search-optimization 2.0.0 → 2.2.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/action.yml +130 -0
- package/package.json +15 -3
- package/src/auto-fix.js +349 -0
- package/src/batch-full-page-audit.js +151 -0
- package/src/citability.js +311 -0
- package/src/citation-check.js +1 -1
- package/src/cli-site-ops-commands.js +391 -2
- package/src/compare.js +175 -0
- package/src/config.js +105 -0
- package/src/crawlers.js +286 -0
- package/src/diagnose.js +221 -0
- package/src/eeat.js +251 -0
- package/src/freshness.js +281 -0
- package/src/full-audit.js +269 -0
- package/src/full-page-audit.js +273 -0
- package/src/heading-structure.js +287 -0
- package/src/index.d.ts +492 -0
- package/src/index.js +24 -0
- package/src/internal-links.js +298 -0
- package/src/page-audit.js +1 -1
- package/src/page-snapshot.js +198 -0
- package/src/pdf-report.js +205 -0
- package/src/platform-ready.js +238 -0
- package/src/plugins.js +126 -0
- package/src/readability.js +252 -0
- package/src/security.js +249 -0
- package/src/sitemap.js +323 -0
- package/src/social-meta.js +293 -0
- package/src/topics.js +275 -0
- package/src/url-onboarding.js +1 -1
- package/src/validate-llms.js +307 -0
- package/src/validate-schema.js +306 -0
|
@@ -31,6 +31,29 @@ import { createRewritePack, renderRewritePackMarkdown, writeRewritePackOutput }
|
|
|
31
31
|
import { renderScanMarkdown, scanProject, writeScanOutput } from "./scan.js";
|
|
32
32
|
import { createSchemaTemplate } from "./schema.js";
|
|
33
33
|
import { analyzeWebsiteUrl, renderWebsiteOnboardingMarkdown, writeWebsiteOnboardingOutput } from "./url-onboarding.js";
|
|
34
|
+
import { analyzeCrawlers, renderCrawlersMarkdown, writeCrawlersOutput } from "./crawlers.js";
|
|
35
|
+
import { analyzeCitability, renderCitabilityMarkdown, writeCitabilityOutput } from "./citability.js";
|
|
36
|
+
import { validateLlmsTxt, renderValidateLlmsMarkdown, writeValidateLlmsOutput } from "./validate-llms.js";
|
|
37
|
+
import { validateSchema, renderValidateSchemaMarkdown, writeValidateSchemaOutput } from "./validate-schema.js";
|
|
38
|
+
import { analyzeEeat, renderEeatMarkdown, writeEeatOutput } from "./eeat.js";
|
|
39
|
+
import { analyzePlatformReadiness, renderPlatformReadyMarkdown, writePlatformReadyOutput } from "./platform-ready.js";
|
|
40
|
+
import { analyzeReadability, renderReadabilityMarkdown, writeReadabilityOutput } from "./readability.js";
|
|
41
|
+
import { analyzeHeadingStructure, renderHeadingStructureMarkdown, writeHeadingStructureOutput } from "./heading-structure.js";
|
|
42
|
+
import { analyzeInternalLinks, renderInternalLinksMarkdown, writeInternalLinksOutput } from "./internal-links.js";
|
|
43
|
+
import { analyzeSocialMeta, renderSocialMetaMarkdown, writeSocialMetaOutput } from "./social-meta.js";
|
|
44
|
+
import { initConfig } from "./config.js";
|
|
45
|
+
import { writePdfReport } from "./pdf-report.js";
|
|
46
|
+
import { fullPageAudit, renderFullPageAuditMarkdown, writeFullPageAuditOutput } from "./full-page-audit.js";
|
|
47
|
+
import { fullAudit, renderFullAuditMarkdown, writeFullAuditOutput } from "./full-audit.js";
|
|
48
|
+
import { analyzeSitemap, renderSitemapMarkdown, writeSitemapOutput } from "./sitemap.js";
|
|
49
|
+
import { analyzeSecurity, renderSecurityMarkdown, writeSecurityOutput } from "./security.js";
|
|
50
|
+
import { analyzeFreshness, renderFreshnessMarkdown, writeFreshnessOutput } from "./freshness.js";
|
|
51
|
+
import { analyzeTopics, renderTopicsMarkdown, writeTopicsOutput } from "./topics.js";
|
|
52
|
+
import { batchFullPageAudit, renderBatchFullPageAuditMarkdown, writeBatchFullPageAuditOutput } from "./batch-full-page-audit.js";
|
|
53
|
+
import { generateAutoFix, renderAutoFixMarkdown, writeAutoFixOutput } from "./auto-fix.js";
|
|
54
|
+
import { savePageSnapshot, buildPageTrend, renderPageTrendMarkdown, writePageTrendOutput } from "./page-snapshot.js";
|
|
55
|
+
import { diagnose, renderDiagnoseMarkdown, writeDiagnoseOutput } from "./diagnose.js";
|
|
56
|
+
import { comparePages, renderCompareMarkdown, writeCompareOutput } from "./compare.js";
|
|
34
57
|
|
|
35
58
|
export const SITE_OPS_HELP_LINES = [
|
|
36
59
|
" geo-ai-search-optimization doctor [--json]",
|
|
@@ -53,7 +76,30 @@ export const SITE_OPS_HELP_LINES = [
|
|
|
53
76
|
" geo-ai-search-optimization watch <project-path> [--debounce <ms>]",
|
|
54
77
|
" geo-ai-search-optimization init-hook [--min-score <n>] [target-dir]",
|
|
55
78
|
" geo-ai-search-optimization citation-check <site-url> --queries <q1,q2,...> [--queries-file <file>] [--engine <perplexity|google|all>] [--json] [--out <file>]",
|
|
56
|
-
" geo-ai-search-optimization citation-monitor <site-url> --queries-file <file> [--save] [--json] [--out <file>]"
|
|
79
|
+
" geo-ai-search-optimization citation-monitor <site-url> --queries-file <file> [--save] [--json] [--out <file>]",
|
|
80
|
+
" geo-ai-search-optimization crawlers <url-or-file> [--strategy <open|selective|block-all>] [--json] [--out <file>]",
|
|
81
|
+
" geo-ai-search-optimization citability <url-or-file> [--json] [--out <file>]",
|
|
82
|
+
" geo-ai-search-optimization validate-llms <url-or-file> [--json] [--out <file>]",
|
|
83
|
+
" geo-ai-search-optimization validate-schema <url-or-file> [--json] [--out <file>]",
|
|
84
|
+
" geo-ai-search-optimization eeat <url-or-file> [--json] [--out <file>]",
|
|
85
|
+
" geo-ai-search-optimization platform-ready <url-or-file> [--json] [--out <file>]",
|
|
86
|
+
" geo-ai-search-optimization readability <url-or-file> [--json] [--out <file>]",
|
|
87
|
+
" geo-ai-search-optimization heading-structure <url-or-file> [--json] [--out <file>]",
|
|
88
|
+
" geo-ai-search-optimization internal-links <url-or-file> [--base-url <url>] [--json] [--out <file>]",
|
|
89
|
+
" geo-ai-search-optimization social-meta <url-or-file> [--json] [--out <file>]",
|
|
90
|
+
" geo-ai-search-optimization init-config [target-dir] [--site-name <name>] [--site-url <url>] [--overwrite] [--json]",
|
|
91
|
+
" geo-ai-search-optimization pdf-report <audit.json> [--out <file>] [--title <title>]",
|
|
92
|
+
" geo-ai-search-optimization full-page-audit <url-or-file> [--base-url <url>] [--save] [--data-dir <dir>] [--json] [--out <file>]",
|
|
93
|
+
" geo-ai-search-optimization full-audit <project-path> [--sample-urls <url1,url2,...>] [--max-file-size <bytes>] [--save] [--data-dir <dir>] [--json] [--out <file>]",
|
|
94
|
+
" geo-ai-search-optimization sitemap <url-or-file> [--json] [--out <file>]",
|
|
95
|
+
" geo-ai-search-optimization security <url-or-file> [--json] [--out <file>]",
|
|
96
|
+
" geo-ai-search-optimization freshness <url-or-file> [--json] [--out <file>]",
|
|
97
|
+
" geo-ai-search-optimization topics <url-or-file> [--json] [--out <file>]",
|
|
98
|
+
" geo-ai-search-optimization batch-full-page-audit <url1> [url2...] [--urls-file <file>] [--concurrency <n>] [--json] [--out <file>]",
|
|
99
|
+
" geo-ai-search-optimization auto-fix <url-or-file> [--json] [--out <file>]",
|
|
100
|
+
" geo-ai-search-optimization page-trend <url-or-file> [--data-dir <dir>] [--last <n>] [--json] [--out <file>]",
|
|
101
|
+
" geo-ai-search-optimization diagnose <url-or-dir-or-file> [--json] [--out <file>]",
|
|
102
|
+
" geo-ai-search-optimization compare <url-or-file-A> <url-or-file-B> [--json] [--out <file>]"
|
|
57
103
|
];
|
|
58
104
|
|
|
59
105
|
const passthroughWriteOutput = async (outputPath) => outputPath;
|
|
@@ -441,6 +487,326 @@ const handleInitHook = createStructuredOutputCommandHandler({
|
|
|
441
487
|
getOutputJson: (args) => hasFlag(args, "--json")
|
|
442
488
|
});
|
|
443
489
|
|
|
490
|
+
const handleCrawlers = createStructuredOutputCommandHandler({
|
|
491
|
+
commandLabel: "AI crawler analysis",
|
|
492
|
+
execute: async (args) => {
|
|
493
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
494
|
+
if (!input) throw new Error("crawlers requires a URL or robots.txt file path");
|
|
495
|
+
return analyzeCrawlers(input, { strategy: getFlagValue(args, "--strategy") || "open" });
|
|
496
|
+
},
|
|
497
|
+
renderMarkdown: renderCrawlersMarkdown,
|
|
498
|
+
writeOutput: writeCrawlersOutput,
|
|
499
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const handleCitability = createStructuredOutputCommandHandler({
|
|
503
|
+
commandLabel: "citability analysis",
|
|
504
|
+
execute: async (args) => {
|
|
505
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
506
|
+
if (!input) throw new Error("citability requires a URL or file path");
|
|
507
|
+
return analyzeCitability(input);
|
|
508
|
+
},
|
|
509
|
+
renderMarkdown: renderCitabilityMarkdown,
|
|
510
|
+
writeOutput: writeCitabilityOutput,
|
|
511
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const handleValidateLlms = createStructuredOutputCommandHandler({
|
|
515
|
+
commandLabel: "llms.txt validation",
|
|
516
|
+
execute: async (args) => {
|
|
517
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
518
|
+
if (!input) throw new Error("validate-llms requires a URL or llms.txt file path");
|
|
519
|
+
return validateLlmsTxt(input);
|
|
520
|
+
},
|
|
521
|
+
renderMarkdown: renderValidateLlmsMarkdown,
|
|
522
|
+
writeOutput: writeValidateLlmsOutput,
|
|
523
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const handleValidateSchema = createStructuredOutputCommandHandler({
|
|
527
|
+
commandLabel: "schema validation",
|
|
528
|
+
execute: async (args) => {
|
|
529
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
530
|
+
if (!input) throw new Error("validate-schema requires a URL or file path");
|
|
531
|
+
return validateSchema(input);
|
|
532
|
+
},
|
|
533
|
+
renderMarkdown: renderValidateSchemaMarkdown,
|
|
534
|
+
writeOutput: writeValidateSchemaOutput,
|
|
535
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const handleEeat = createStructuredOutputCommandHandler({
|
|
539
|
+
commandLabel: "E-E-A-T analysis",
|
|
540
|
+
execute: async (args) => {
|
|
541
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
542
|
+
if (!input) throw new Error("eeat requires a URL or file path");
|
|
543
|
+
return analyzeEeat(input);
|
|
544
|
+
},
|
|
545
|
+
renderMarkdown: renderEeatMarkdown,
|
|
546
|
+
writeOutput: writeEeatOutput,
|
|
547
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const handlePlatformReady = createStructuredOutputCommandHandler({
|
|
551
|
+
commandLabel: "platform readiness",
|
|
552
|
+
execute: async (args) => {
|
|
553
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
554
|
+
if (!input) throw new Error("platform-ready requires a URL or file path");
|
|
555
|
+
return analyzePlatformReadiness(input);
|
|
556
|
+
},
|
|
557
|
+
renderMarkdown: renderPlatformReadyMarkdown,
|
|
558
|
+
writeOutput: writePlatformReadyOutput,
|
|
559
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const handleReadability = createStructuredOutputCommandHandler({
|
|
563
|
+
commandLabel: "readability analysis",
|
|
564
|
+
execute: async (args) => {
|
|
565
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
566
|
+
if (!input) throw new Error("readability requires a URL or file path");
|
|
567
|
+
return analyzeReadability(input);
|
|
568
|
+
},
|
|
569
|
+
renderMarkdown: renderReadabilityMarkdown,
|
|
570
|
+
writeOutput: writeReadabilityOutput,
|
|
571
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const handleHeadingStructure = createStructuredOutputCommandHandler({
|
|
575
|
+
commandLabel: "heading structure",
|
|
576
|
+
execute: async (args) => {
|
|
577
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
578
|
+
if (!input) throw new Error("heading-structure requires a URL or file path");
|
|
579
|
+
return analyzeHeadingStructure(input);
|
|
580
|
+
},
|
|
581
|
+
renderMarkdown: renderHeadingStructureMarkdown,
|
|
582
|
+
writeOutput: writeHeadingStructureOutput,
|
|
583
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const handleInternalLinks = createStructuredOutputCommandHandler({
|
|
587
|
+
commandLabel: "internal links",
|
|
588
|
+
execute: async (args) => {
|
|
589
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
590
|
+
if (!input) throw new Error("internal-links requires a URL or file path");
|
|
591
|
+
return analyzeInternalLinks(input, { baseUrl: getFlagValue(args, "--base-url") });
|
|
592
|
+
},
|
|
593
|
+
renderMarkdown: renderInternalLinksMarkdown,
|
|
594
|
+
writeOutput: writeInternalLinksOutput,
|
|
595
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const handleSocialMeta = createStructuredOutputCommandHandler({
|
|
599
|
+
commandLabel: "social meta",
|
|
600
|
+
execute: async (args) => {
|
|
601
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
602
|
+
if (!input) throw new Error("social-meta requires a URL or file path");
|
|
603
|
+
return analyzeSocialMeta(input);
|
|
604
|
+
},
|
|
605
|
+
renderMarkdown: renderSocialMetaMarkdown,
|
|
606
|
+
writeOutput: writeSocialMetaOutput,
|
|
607
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const handleInitConfig = createActionCommandHandler({
|
|
611
|
+
execute: async (args) =>
|
|
612
|
+
initConfig({
|
|
613
|
+
targetDir: args.find((value) => !value.startsWith("-")) || ".",
|
|
614
|
+
siteName: getFlagValue(args, "--site-name"),
|
|
615
|
+
siteUrl: getFlagValue(args, "--site-url"),
|
|
616
|
+
overwrite: hasFlag(args, "--overwrite")
|
|
617
|
+
}),
|
|
618
|
+
getOutputJson: (args) => hasFlag(args, "--json"),
|
|
619
|
+
renderText: (result) => `Created config at ${result.outputPath}\n`
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const handlePdfReport = createActionCommandHandler({
|
|
623
|
+
execute: async (args) => {
|
|
624
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
625
|
+
if (!input) throw new Error("pdf-report requires an audit JSON file path");
|
|
626
|
+
return writePdfReport(input, getFlagValue(args, "--out"), {
|
|
627
|
+
title: getFlagValue(args, "--title")
|
|
628
|
+
});
|
|
629
|
+
},
|
|
630
|
+
getOutputJson: (args) => hasFlag(args, "--json"),
|
|
631
|
+
renderText: (result) => `Generated PDF-ready HTML report at ${result.outputPath}\n`
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
const handleSitemap = createStructuredOutputCommandHandler({
|
|
635
|
+
commandLabel: "sitemap analysis",
|
|
636
|
+
execute: async (args) => {
|
|
637
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
638
|
+
if (!input) throw new Error("sitemap requires a URL or sitemap file path");
|
|
639
|
+
return analyzeSitemap(input);
|
|
640
|
+
},
|
|
641
|
+
renderMarkdown: renderSitemapMarkdown,
|
|
642
|
+
writeOutput: writeSitemapOutput,
|
|
643
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const handleSecurity = createStructuredOutputCommandHandler({
|
|
647
|
+
commandLabel: "security analysis",
|
|
648
|
+
execute: async (args) => {
|
|
649
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
650
|
+
if (!input) throw new Error("security requires a URL or file path");
|
|
651
|
+
return analyzeSecurity(input);
|
|
652
|
+
},
|
|
653
|
+
renderMarkdown: renderSecurityMarkdown,
|
|
654
|
+
writeOutput: writeSecurityOutput,
|
|
655
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const handleFreshness = createStructuredOutputCommandHandler({
|
|
659
|
+
commandLabel: "freshness analysis",
|
|
660
|
+
execute: async (args) => {
|
|
661
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
662
|
+
if (!input) throw new Error("freshness requires a URL or file path");
|
|
663
|
+
return analyzeFreshness(input);
|
|
664
|
+
},
|
|
665
|
+
renderMarkdown: renderFreshnessMarkdown,
|
|
666
|
+
writeOutput: writeFreshnessOutput,
|
|
667
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const handleTopics = createStructuredOutputCommandHandler({
|
|
671
|
+
commandLabel: "topic analysis",
|
|
672
|
+
execute: async (args) => {
|
|
673
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
674
|
+
if (!input) throw new Error("topics requires a URL or file path");
|
|
675
|
+
return analyzeTopics(input);
|
|
676
|
+
},
|
|
677
|
+
renderMarkdown: renderTopicsMarkdown,
|
|
678
|
+
writeOutput: writeTopicsOutput,
|
|
679
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const handleBatchFullPageAudit = createStructuredOutputCommandHandler({
|
|
683
|
+
commandLabel: "batch full page audit",
|
|
684
|
+
execute: async (args) => {
|
|
685
|
+
const nonFlags = args.filter((value) => !value.startsWith("-"));
|
|
686
|
+
const urlsFile = getFlagValue(args, "--urls-file");
|
|
687
|
+
let urls = [...nonFlags];
|
|
688
|
+
|
|
689
|
+
if (urlsFile) {
|
|
690
|
+
const { readFile } = await import("node:fs/promises");
|
|
691
|
+
const fileContent = await readFile(urlsFile, "utf8");
|
|
692
|
+
const fileUrls = fileContent.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
693
|
+
urls = [...urls, ...fileUrls];
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (urls.length === 0) throw new Error("batch-full-page-audit requires at least one URL");
|
|
697
|
+
|
|
698
|
+
const concurrencyValue = getFlagValue(args, "--concurrency");
|
|
699
|
+
return batchFullPageAudit(urls, {
|
|
700
|
+
concurrency: concurrencyValue ? parsePositiveInteger(concurrencyValue, "--concurrency") : 2
|
|
701
|
+
});
|
|
702
|
+
},
|
|
703
|
+
renderMarkdown: renderBatchFullPageAuditMarkdown,
|
|
704
|
+
writeOutput: writeBatchFullPageAuditOutput,
|
|
705
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
const handleDiagnose = createStructuredOutputCommandHandler({
|
|
709
|
+
commandLabel: "diagnose",
|
|
710
|
+
execute: async (args) => {
|
|
711
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
712
|
+
if (!input) throw new Error("diagnose requires a URL, file path, or directory");
|
|
713
|
+
return diagnose(input);
|
|
714
|
+
},
|
|
715
|
+
renderMarkdown: renderDiagnoseMarkdown,
|
|
716
|
+
writeOutput: writeDiagnoseOutput,
|
|
717
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const handleCompare = createStructuredOutputCommandHandler({
|
|
721
|
+
commandLabel: "compare",
|
|
722
|
+
execute: async (args) => {
|
|
723
|
+
const nonFlags = args.filter((value) => !value.startsWith("-"));
|
|
724
|
+
if (nonFlags.length < 2) throw new Error("compare requires two URLs or file paths: <A> <B>");
|
|
725
|
+
return comparePages(nonFlags[0], nonFlags[1]);
|
|
726
|
+
},
|
|
727
|
+
renderMarkdown: renderCompareMarkdown,
|
|
728
|
+
writeOutput: writeCompareOutput,
|
|
729
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const handleAutoFix = createStructuredOutputCommandHandler({
|
|
733
|
+
commandLabel: "auto-fix",
|
|
734
|
+
execute: async (args) => {
|
|
735
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
736
|
+
if (!input) throw new Error("auto-fix requires a URL or file path");
|
|
737
|
+
return generateAutoFix(input);
|
|
738
|
+
},
|
|
739
|
+
renderMarkdown: renderAutoFixMarkdown,
|
|
740
|
+
writeOutput: writeAutoFixOutput,
|
|
741
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
const handlePageTrend = createStructuredOutputCommandHandler({
|
|
745
|
+
commandLabel: "page trend",
|
|
746
|
+
execute: async (args) => {
|
|
747
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
748
|
+
if (!input) throw new Error("page-trend requires a URL or file path (same input used for full-page-audit)");
|
|
749
|
+
const lastValue = getFlagValue(args, "--last");
|
|
750
|
+
return buildPageTrend(input, {
|
|
751
|
+
dataDir: getFlagValue(args, "--data-dir") || undefined,
|
|
752
|
+
last: lastValue ? parsePositiveInteger(lastValue, "--last") : 0
|
|
753
|
+
});
|
|
754
|
+
},
|
|
755
|
+
renderMarkdown: renderPageTrendMarkdown,
|
|
756
|
+
writeOutput: writePageTrendOutput,
|
|
757
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const handleFullPageAudit = createStructuredOutputCommandHandler({
|
|
761
|
+
commandLabel: "full page audit",
|
|
762
|
+
execute: async (args) => {
|
|
763
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
764
|
+
if (!input) throw new Error("full-page-audit requires a URL or file path");
|
|
765
|
+
const result = await fullPageAudit(input, { baseUrl: getFlagValue(args, "--base-url") });
|
|
766
|
+
|
|
767
|
+
if (hasFlag(args, "--save")) {
|
|
768
|
+
const saved = await savePageSnapshot(result, {
|
|
769
|
+
dataDir: getFlagValue(args, "--data-dir") || undefined
|
|
770
|
+
});
|
|
771
|
+
process.stderr.write(`快照已保存:${saved.path}\n`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return result;
|
|
775
|
+
},
|
|
776
|
+
renderMarkdown: renderFullPageAuditMarkdown,
|
|
777
|
+
writeOutput: writeFullPageAuditOutput,
|
|
778
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const handleFullAudit = createStructuredOutputCommandHandler({
|
|
782
|
+
commandLabel: "full audit",
|
|
783
|
+
execute: async (args) => {
|
|
784
|
+
const projectPath = args.find((value) => !value.startsWith("-"));
|
|
785
|
+
if (!projectPath) throw new Error("full-audit requires a project path");
|
|
786
|
+
|
|
787
|
+
const sampleUrlsRaw = getFlagValue(args, "--sample-urls");
|
|
788
|
+
const sampleUrls = sampleUrlsRaw ? sampleUrlsRaw.split(",").map((u) => u.trim()).filter(Boolean) : [];
|
|
789
|
+
|
|
790
|
+
const result = await fullAudit(projectPath, {
|
|
791
|
+
sampleUrls,
|
|
792
|
+
maxFileSize: getFlagValue(args, "--max-file-size")
|
|
793
|
+
? parsePositiveInteger(getFlagValue(args, "--max-file-size"), "--max-file-size")
|
|
794
|
+
: undefined
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
if (hasFlag(args, "--save")) {
|
|
798
|
+
const dataDir = getFlagValue(args, "--data-dir") || undefined;
|
|
799
|
+
const saved = await saveSnapshot(result, { dataDir });
|
|
800
|
+
process.stderr.write(`快照已保存:${saved.path}\n`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return result;
|
|
804
|
+
},
|
|
805
|
+
renderMarkdown: renderFullAuditMarkdown,
|
|
806
|
+
writeOutput: writeFullAuditOutput,
|
|
807
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
808
|
+
});
|
|
809
|
+
|
|
444
810
|
export const SITE_OPS_COMMAND_HANDLERS = {
|
|
445
811
|
doctor: handleDoctor,
|
|
446
812
|
"quick-start": handleQuickStart,
|
|
@@ -462,5 +828,28 @@ export const SITE_OPS_COMMAND_HANDLERS = {
|
|
|
462
828
|
watch: handleWatch,
|
|
463
829
|
"init-hook": handleInitHook,
|
|
464
830
|
"citation-check": handleCitationCheck,
|
|
465
|
-
"citation-monitor": handleCitationMonitor
|
|
831
|
+
"citation-monitor": handleCitationMonitor,
|
|
832
|
+
crawlers: handleCrawlers,
|
|
833
|
+
citability: handleCitability,
|
|
834
|
+
"validate-llms": handleValidateLlms,
|
|
835
|
+
"validate-schema": handleValidateSchema,
|
|
836
|
+
eeat: handleEeat,
|
|
837
|
+
"platform-ready": handlePlatformReady,
|
|
838
|
+
readability: handleReadability,
|
|
839
|
+
"heading-structure": handleHeadingStructure,
|
|
840
|
+
"internal-links": handleInternalLinks,
|
|
841
|
+
"social-meta": handleSocialMeta,
|
|
842
|
+
"init-config": handleInitConfig,
|
|
843
|
+
"pdf-report": handlePdfReport,
|
|
844
|
+
"full-page-audit": handleFullPageAudit,
|
|
845
|
+
"full-audit": handleFullAudit,
|
|
846
|
+
sitemap: handleSitemap,
|
|
847
|
+
security: handleSecurity,
|
|
848
|
+
freshness: handleFreshness,
|
|
849
|
+
topics: handleTopics,
|
|
850
|
+
"batch-full-page-audit": handleBatchFullPageAudit,
|
|
851
|
+
"auto-fix": handleAutoFix,
|
|
852
|
+
"page-trend": handlePageTrend,
|
|
853
|
+
diagnose: handleDiagnose,
|
|
854
|
+
compare: handleCompare
|
|
466
855
|
};
|
package/src/compare.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { writeScanOutput } from "./scan.js";
|
|
2
|
+
import { fullPageAudit } from "./full-page-audit.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compare two pages across all 12 dimensions.
|
|
6
|
+
* Produces a side-by-side analysis with delta scoring.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export async function comparePages(inputA, inputB, options = {}) {
|
|
10
|
+
const [resultA, resultB] = await Promise.all([
|
|
11
|
+
fullPageAudit(inputA, options),
|
|
12
|
+
fullPageAudit(inputB, options)
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const dimensions = {};
|
|
16
|
+
const allDimKeys = new Set([
|
|
17
|
+
...Object.keys(resultA.dimensions),
|
|
18
|
+
...Object.keys(resultB.dimensions)
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
let aWins = 0;
|
|
22
|
+
let bWins = 0;
|
|
23
|
+
let ties = 0;
|
|
24
|
+
|
|
25
|
+
for (const key of allDimKeys) {
|
|
26
|
+
const scoreA = resultA.dimensions[key]?.score ?? 0;
|
|
27
|
+
const scoreB = resultB.dimensions[key]?.score ?? 0;
|
|
28
|
+
const delta = scoreA - scoreB;
|
|
29
|
+
const winner = delta > 0 ? "A" : delta < 0 ? "B" : "tie";
|
|
30
|
+
|
|
31
|
+
if (winner === "A") aWins++;
|
|
32
|
+
else if (winner === "B") bWins++;
|
|
33
|
+
else ties++;
|
|
34
|
+
|
|
35
|
+
dimensions[key] = {
|
|
36
|
+
scoreA,
|
|
37
|
+
scoreB,
|
|
38
|
+
delta,
|
|
39
|
+
winner,
|
|
40
|
+
labelA: resultA.dimensions[key]?.label ?? "",
|
|
41
|
+
labelB: resultB.dimensions[key]?.label ?? ""
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const compositeA = resultA.compositeScore;
|
|
46
|
+
const compositeB = resultB.compositeScore;
|
|
47
|
+
const compositeDelta = compositeA - compositeB;
|
|
48
|
+
|
|
49
|
+
// Identify where each page is stronger
|
|
50
|
+
const aStrengths = Object.entries(dimensions)
|
|
51
|
+
.filter(([, d]) => d.delta > 10)
|
|
52
|
+
.sort((a, b) => b[1].delta - a[1].delta)
|
|
53
|
+
.map(([key, d]) => ({ dimension: key, delta: d.delta }));
|
|
54
|
+
|
|
55
|
+
const bStrengths = Object.entries(dimensions)
|
|
56
|
+
.filter(([, d]) => d.delta < -10)
|
|
57
|
+
.sort((a, b) => a[1].delta - b[1].delta)
|
|
58
|
+
.map(([key, d]) => ({ dimension: key, delta: Math.abs(d.delta) }));
|
|
59
|
+
|
|
60
|
+
// Platform comparison
|
|
61
|
+
const platformComparison = [];
|
|
62
|
+
if (resultA.platformSummary && resultB.platformSummary) {
|
|
63
|
+
for (let i = 0; i < resultA.platformSummary.length; i++) {
|
|
64
|
+
const pA = resultA.platformSummary[i];
|
|
65
|
+
const pB = resultB.platformSummary[i];
|
|
66
|
+
if (pA && pB) {
|
|
67
|
+
platformComparison.push({
|
|
68
|
+
platform: pA.platform,
|
|
69
|
+
scoreA: pA.score,
|
|
70
|
+
scoreB: pB.score,
|
|
71
|
+
delta: pA.score - pB.score,
|
|
72
|
+
readinessA: pA.readiness,
|
|
73
|
+
readinessB: pB.readiness
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const overallWinner = compositeDelta > 5 ? "A" : compositeDelta < -5 ? "B" : "tie";
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
kind: "geo-compare",
|
|
83
|
+
inputA,
|
|
84
|
+
inputB,
|
|
85
|
+
compositeA,
|
|
86
|
+
compositeB,
|
|
87
|
+
compositeDelta,
|
|
88
|
+
overallWinner,
|
|
89
|
+
dimensionWins: { A: aWins, B: bWins, ties },
|
|
90
|
+
dimensions,
|
|
91
|
+
aStrengths,
|
|
92
|
+
bStrengths,
|
|
93
|
+
platformComparison,
|
|
94
|
+
summary: overallWinner === "tie"
|
|
95
|
+
? `Near parity. A: ${compositeA}/100, B: ${compositeB}/100 (Δ${compositeDelta >= 0 ? "+" : ""}${compositeDelta}).`
|
|
96
|
+
: `${overallWinner === "A" ? "Page A" : "Page B"} leads. A: ${compositeA}/100, B: ${compositeB}/100 (Δ${compositeDelta >= 0 ? "+" : ""}${compositeDelta}). Dimension wins: A=${aWins}, B=${bWins}, Ties=${ties}.`
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function renderCompareMarkdown(report) {
|
|
101
|
+
const lines = [
|
|
102
|
+
"# GEO Page Comparison",
|
|
103
|
+
"",
|
|
104
|
+
`- **Page A**: \`${report.inputA}\``,
|
|
105
|
+
`- **Page B**: \`${report.inputB}\``,
|
|
106
|
+
`- Summary: ${report.summary}`,
|
|
107
|
+
"",
|
|
108
|
+
"## Composite Scores",
|
|
109
|
+
"",
|
|
110
|
+
`| | Page A | Page B | Delta |`,
|
|
111
|
+
`|--|--------|--------|-------|`,
|
|
112
|
+
`| **Composite** | **${report.compositeA}** | **${report.compositeB}** | ${report.compositeDelta >= 0 ? "+" : ""}${report.compositeDelta} |`,
|
|
113
|
+
""
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const dimLabels = {
|
|
117
|
+
base: "Base", citability: "Citability", eeat: "E-E-A-T", readability: "Readability",
|
|
118
|
+
headingStructure: "Headings", internalLinks: "Links", socialMeta: "Social",
|
|
119
|
+
platformReady: "Platforms", schema: "Schema", freshness: "Freshness",
|
|
120
|
+
security: "Security", topics: "Topics"
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
lines.push(
|
|
124
|
+
"## Dimension Comparison",
|
|
125
|
+
"",
|
|
126
|
+
"| Dimension | Page A | Page B | Δ | Winner |",
|
|
127
|
+
"|-----------|--------|--------|---|--------|"
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const sorted = Object.entries(report.dimensions)
|
|
131
|
+
.sort((a, b) => Math.abs(b[1].delta) - Math.abs(a[1].delta));
|
|
132
|
+
|
|
133
|
+
for (const [key, d] of sorted) {
|
|
134
|
+
const winIcon = d.winner === "A" ? "◀️ A" : d.winner === "B" ? "B ▶️" : "—";
|
|
135
|
+
lines.push(`| ${dimLabels[key] || key} | ${d.scoreA} | ${d.scoreB} | ${d.delta >= 0 ? "+" : ""}${d.delta} | ${winIcon} |`);
|
|
136
|
+
}
|
|
137
|
+
lines.push("");
|
|
138
|
+
|
|
139
|
+
if (report.aStrengths.length > 0) {
|
|
140
|
+
lines.push("## Page A Strengths", "");
|
|
141
|
+
for (const s of report.aStrengths) {
|
|
142
|
+
lines.push(`- **${dimLabels[s.dimension] || s.dimension}**: +${s.delta} points ahead`);
|
|
143
|
+
}
|
|
144
|
+
lines.push("");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (report.bStrengths.length > 0) {
|
|
148
|
+
lines.push("## Page B Strengths", "");
|
|
149
|
+
for (const s of report.bStrengths) {
|
|
150
|
+
lines.push(`- **${dimLabels[s.dimension] || s.dimension}**: +${s.delta} points ahead`);
|
|
151
|
+
}
|
|
152
|
+
lines.push("");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (report.platformComparison.length > 0) {
|
|
156
|
+
lines.push(
|
|
157
|
+
"## Platform Readiness Comparison",
|
|
158
|
+
"",
|
|
159
|
+
"| Platform | A Score | B Score | Δ | A Status | B Status |",
|
|
160
|
+
"|----------|---------|---------|---|----------|----------|"
|
|
161
|
+
);
|
|
162
|
+
for (const p of report.platformComparison) {
|
|
163
|
+
lines.push(`| ${p.platform} | ${p.scoreA} | ${p.scoreB} | ${p.delta >= 0 ? "+" : ""}${p.delta} | ${p.readinessA} | ${p.readinessB} |`);
|
|
164
|
+
}
|
|
165
|
+
lines.push("");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
lines.push(`## Verdict`, "", `**Dimension wins**: A=${report.dimensionWins.A}, B=${report.dimensionWins.B}, Ties=${report.dimensionWins.ties}`, "");
|
|
169
|
+
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function writeCompareOutput(outputPath, content) {
|
|
174
|
+
return writeScanOutput(outputPath, content);
|
|
175
|
+
}
|