gsc-cli 2.0.2 → 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.
- checksums.yaml +4 -4
- data/AUTH.md +205 -0
- data/FUNDING.md +120 -0
- data/README.md +463 -299
- data/bin/gsc +29158 -4921
- data/dist/gsc +29158 -4921
- data/lib/gsc/aio_hunter.rb +343 -0
- data/lib/gsc/answer_synthesizer.rb +157 -0
- data/lib/gsc/api.rb +53 -1
- data/lib/gsc/auth.rb +26 -0
- data/lib/gsc/backlinks_manager.rb +96 -0
- data/lib/gsc/brand_segmenter.rb +140 -0
- data/lib/gsc/cache_manager.rb +806 -0
- data/lib/gsc/cannibalization_analyzer.rb +141 -0
- data/lib/gsc/canonical_chains.rb +367 -0
- data/lib/gsc/citation_simulator.rb +339 -0
- data/lib/gsc/cli/aio_hunter.rb +154 -0
- data/lib/gsc/cli/analytics.rb +788 -0
- data/lib/gsc/cli/audit.rb +1976 -0
- data/lib/gsc/cli/base.rb +384 -0
- data/lib/gsc/cli/cache.rb +266 -0
- data/lib/gsc/cli/canonical.rb +223 -0
- data/lib/gsc/cli/citation_simulator.rb +152 -0
- data/lib/gsc/cli/dashboard.rb +354 -0
- data/lib/gsc/cli/doctor.rb +129 -0
- data/lib/gsc/cli/eeat.rb +125 -0
- data/lib/gsc/cli/ga4.rb +852 -0
- data/lib/gsc/cli/growth.rb +650 -0
- data/lib/gsc/cli/hreflang.rb +164 -0
- data/lib/gsc/cli/image_seo.rb +162 -0
- data/lib/gsc/cli/indexing.rb +458 -0
- data/lib/gsc/cli/intent_shift.rb +125 -0
- data/lib/gsc/cli/keyword_value.rb +134 -0
- data/lib/gsc/cli/keywords.rb +795 -0
- data/lib/gsc/cli/landing_roi.rb +308 -0
- data/lib/gsc/cli/low_ctr.rb +213 -0
- data/lib/gsc/cli/mobile_parity.rb +150 -0
- data/lib/gsc/cli/report.rb +100 -0
- data/lib/gsc/cli/rich_results.rb +172 -0
- data/lib/gsc/cli/schema_generate.rb +149 -0
- data/lib/gsc/cli/seasonal.rb +232 -0
- data/lib/gsc/cli/security.rb +153 -0
- data/lib/gsc/cli/setup.rb +1291 -0
- data/lib/gsc/cli/sitemap_tree.rb +143 -0
- data/lib/gsc/cli/skill_pack.rb +62 -0
- data/lib/gsc/cli/soft_404.rb +199 -0
- data/lib/gsc/cli/sparkline.rb +227 -0
- data/lib/gsc/cli/watchdog.rb +150 -0
- data/lib/gsc/cli/zombie_purger.rb +208 -0
- data/lib/gsc/cli.rb +706 -5111
- data/lib/gsc/cli_advanced.rb +1513 -0
- data/lib/gsc/client.rb +17 -2
- data/lib/gsc/color.rb +16 -1
- data/lib/gsc/command_registry.rb +47 -9
- data/lib/gsc/config.rb +11 -2
- data/lib/gsc/content_gap.rb +112 -0
- data/lib/gsc/ctr_curve.rb +115 -0
- data/lib/gsc/decay_predictor.rb +322 -0
- data/lib/gsc/doctor.rb +434 -0
- data/lib/gsc/eeat_auditor.rb +428 -0
- data/lib/gsc/entity_auditor.rb +229 -0
- data/lib/gsc/firewall_scanner.rb +733 -0
- data/lib/gsc/geo_auditor.rb +368 -0
- data/lib/gsc/google_suggest.rb +109 -0
- data/lib/gsc/google_trends.rb +8 -1
- data/lib/gsc/heading_validator.rb +283 -0
- data/lib/gsc/hreflang_validator.rb +412 -0
- data/lib/gsc/image_seo.rb +286 -0
- data/lib/gsc/indexing_queue.rb +179 -0
- data/lib/gsc/indexnow.rb +93 -0
- data/lib/gsc/intent_shift.rb +188 -0
- data/lib/gsc/internal_links.rb +249 -0
- data/lib/gsc/keyword_value.rb +191 -0
- data/lib/gsc/landing_roi.rb +195 -0
- data/lib/gsc/llms_generator.rb +425 -0
- data/lib/gsc/low_ctr_rewriter.rb +408 -0
- data/lib/gsc/mobile_parity.rb +222 -0
- data/lib/gsc/network_tracer.rb +93 -0
- data/lib/gsc/open_page_rank.rb +72 -0
- data/lib/gsc/page_analyzer.rb +47 -7
- data/lib/gsc/page_comparator.rb +108 -0
- data/lib/gsc/page_speed.rb +110 -0
- data/lib/gsc/prompts.rb +38 -29
- data/lib/gsc/questions_harvester.rb +178 -0
- data/lib/gsc/report_generator.rb +461 -0
- data/lib/gsc/rich_results.rb +388 -0
- data/lib/gsc/robots_checker.rb +114 -0
- data/lib/gsc/schema_generator.rb +788 -0
- data/lib/gsc/schema_validator.rb +120 -0
- data/lib/gsc/seasonal_predictor.rb +381 -0
- data/lib/gsc/security_scanner.rb +496 -0
- data/lib/gsc/serp_feature_detector.rb +359 -0
- data/lib/gsc/serp_preview.rb +152 -0
- data/lib/gsc/site_crawler.rb +113 -21
- data/lib/gsc/sitemap_loader.rb +15 -4
- data/lib/gsc/sitemap_tree.rb +301 -0
- data/lib/gsc/skill_pack.rb +195 -0
- data/lib/gsc/soft_404_analyzer.rb +385 -0
- data/lib/gsc/sparkline.rb +171 -0
- data/lib/gsc/speed_correlator.rb +416 -0
- data/lib/gsc/striking_playbook.rb +190 -0
- data/lib/gsc/title_optimizer.rb +420 -0
- data/lib/gsc/vault.rb +260 -0
- data/lib/gsc/version.rb +1 -1
- data/lib/gsc/watchdog.rb +235 -0
- data/lib/gsc/zombie_purger.rb +366 -0
- data/lib/gsc.rb +144 -0
- metadata +91 -2
data/lib/gsc/client.rb
CHANGED
|
@@ -55,12 +55,27 @@ module GSC
|
|
|
55
55
|
# Transparent gzip decompression
|
|
56
56
|
raw_body = response.body
|
|
57
57
|
body_str = if response['content-encoding'] =~ /gzip/i && raw_body && !raw_body.empty?
|
|
58
|
-
|
|
58
|
+
begin
|
|
59
|
+
Zlib::GzipReader.new(StringIO.new(raw_body)).read
|
|
60
|
+
rescue Zlib::GzipFile::Error, Zlib::Error
|
|
61
|
+
raw_body
|
|
62
|
+
end
|
|
59
63
|
else
|
|
60
64
|
raw_body
|
|
61
65
|
end
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
body_str = body_str.to_s.dup.force_encoding('UTF-8').scrub if body_str
|
|
68
|
+
|
|
69
|
+
body = if body_str && !body_str.strip.empty?
|
|
70
|
+
begin
|
|
71
|
+
parsed = JSON.parse(body_str)
|
|
72
|
+
parsed.is_a?(Hash) || parsed.is_a?(Array) ? parsed : { 'value' => parsed }
|
|
73
|
+
rescue JSON::ParserError, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
74
|
+
{ 'error' => { 'message' => body_str }, 'raw' => body_str }
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
{}
|
|
78
|
+
end
|
|
64
79
|
|
|
65
80
|
{
|
|
66
81
|
ok: response.is_a?(Net::HTTPSuccess),
|
data/lib/gsc/color.rb
CHANGED
|
@@ -16,7 +16,22 @@ module GSC
|
|
|
16
16
|
GRAY = "\e[90m"
|
|
17
17
|
|
|
18
18
|
def self.c(text, *styles)
|
|
19
|
-
|
|
19
|
+
t = text.to_s.dup.force_encoding('UTF-8').scrub
|
|
20
|
+
"#{styles.join}#{t}#{RESET}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.cyan(str); c(str, CYAN); end
|
|
24
|
+
def self.yellow(str); c(str, YELLOW); end
|
|
25
|
+
def self.green(str); c(str, GREEN); end
|
|
26
|
+
def self.red(str); c(str, RED); end
|
|
27
|
+
def self.bold(str); c(str, BOLD); end
|
|
28
|
+
def self.gray(str); c(str, GRAY); end
|
|
29
|
+
def self.magenta(str); c(str, MAGENTA); end
|
|
30
|
+
def self.white(str); c(str, WHITE); end
|
|
31
|
+
def self.dim(str); c(str, DIM); end
|
|
32
|
+
|
|
33
|
+
def self.strip_ansi(str)
|
|
34
|
+
str.to_s.gsub(/\e\[[0-9;]*[mK]/, '')
|
|
20
35
|
end
|
|
21
36
|
end
|
|
22
37
|
end
|
data/lib/gsc/command_registry.rb
CHANGED
|
@@ -6,9 +6,9 @@ COMMAND_REGISTRY = [
|
|
|
6
6
|
{
|
|
7
7
|
category: "Google Trends & Keyword Demand",
|
|
8
8
|
commands: [
|
|
9
|
-
{ name: "ke <seed|file>", shortcut: "ke", desc: "Keywords Everywhere: Exact monthly volume, CPC, competition & GSC correlation", flags: ["--country", "--limit", "--json"] },
|
|
10
|
-
{ name: "ke-credits", shortcut: "ke-credits", desc: "Check remaining Keywords Everywhere account API credits", flags: ["--json"] },
|
|
11
|
-
{ name: "connect ke [key]", shortcut: "connect ke", desc: "Connect Keywords Everywhere API key and save to config.json", flags: [] },
|
|
9
|
+
{ name: "ke <seed|file>", shortcut: "ke", desc: "Keywords Everywhere: Exact monthly volume, CPC, competition & GSC correlation", flags: ["--country", "--limit", "--json"] },
|
|
10
|
+
{ name: "ke-credits", shortcut: "ke-credits", desc: "Check remaining Keywords Everywhere account API credits", flags: ["--json"] },
|
|
11
|
+
{ name: "connect ke [key]", shortcut: "connect ke", desc: "Connect Keywords Everywhere API key and save to config.json", flags: [] },
|
|
12
12
|
{ name: "trends <query>", shortcut: "tr", desc: "Live Google Trends: 5yr/1yr demand trajectory, velocity & breakout queries", flags: ["--geo", "--time", "--json"] },
|
|
13
13
|
{ name: "planner <seed>", shortcut: "kp", desc: "Free keyword planner: intent expansion & GSC ranking correlation", flags: ["--country", "--limit", "--json"] },
|
|
14
14
|
{ name: "planner-import <file>", shortcut: "pi", desc: "Import Google Ads CSV or Markdown table to score & rank opportunities", flags: ["--limit", "--json"] }
|
|
@@ -18,12 +18,18 @@ COMMAND_REGISTRY = [
|
|
|
18
18
|
category: "Search Console Intelligence",
|
|
19
19
|
commands: [
|
|
20
20
|
{ name: "performance", shortcut: "p", desc: "Executive dashboard: Clicks, Impressions, CTR, Position", flags: ["--days", "--limit", "--csv"] },
|
|
21
|
-
{ name: "top-queries", shortcut: "t", desc: "Top search queries, rankings, and CTR", flags: ["--days", "--limit", "-s", "--order", "--csv"] },
|
|
21
|
+
{ name: "top-queries", shortcut: "t", desc: "Top search queries, rankings, and CTR", flags: ["--days", "--limit", "-s", "--order", "--brand", "--non-brand", "--brand-name", "--csv"] },
|
|
22
|
+
{ name: "brand", shortcut: "b", desc: "Brand vs. non-brand search query segmentation & share", flags: ["--days", "--limit", "--brand-name", "--json"] },
|
|
23
|
+
{ name: "ctr-curve", shortcut: nil, desc: "Expected SERP CTR curve & click gain simulator", flags: ["--target-pos", "--min-imp", "--limit", "--days", "--csv"] },
|
|
22
24
|
{ name: "top-pages", shortcut: nil, desc: "Top landing pages driving organic search clicks", flags: ["--days", "--limit", "-s", "--order", "--csv"] },
|
|
23
25
|
{ name: "opportunities", shortcut: "o", desc: "Striking-distance queries (Pos 7-20) to push to Top 3", flags: ["--days", "--min-imp", "--min-pos", "--max-pos"] },
|
|
26
|
+
{ name: "strike", shortcut: "sp", desc: "Striking-distance tactical playbook: Title rewrites, heading recipes & internal link anchors (Pos 7-20)", flags: ["--days", "--min-imp", "--min-pos", "--max-pos", "--limit", "--csv", "--json"] },
|
|
24
27
|
{ name: "underperformers", shortcut: "u", desc: "High-ranking queries (Top 10) with low CTR (title tag wins)", flags: ["--days", "--min-imp"] },
|
|
28
|
+
{ name: "low-ctr [query/url]", shortcut: "lc", desc: "Low-CTR High-Impression Title Tag Rewriter: 3 SERP-safe hook titles (<560px), click hemorrhage & revenue recovery", flags: ["--min-imp", "--cpc", "--limit", "--brand", "--csv", "--json"] },
|
|
25
29
|
{ name: "cannibalization", shortcut: "c", desc: "Detect internal URLs competing for the same keywords", flags: ["--days", "--min-imp"] },
|
|
30
|
+
{ name: "questions-harvest [page]", shortcut: "qh", desc: "Harvest question queries from GSC and generate FAQPage JSON-LD schema", flags: ["--days", "--min-imp", "--limit", "--generate", "--csv", "--json"] },
|
|
26
31
|
{ name: "decay", shortcut: "d", desc: "Period-over-period decay detection (decaying vs surging)", flags: ["--compare"] },
|
|
32
|
+
{ name: "sparklines [query|page]", shortcut: "spark", desc: "Terminal ASCII Sparklines & Multi-Horizon Trajectory Graphs (Clicks, Imp, CTR, Pos)", flags: ["--days", "--metric", "--height", "--data", "--csv", "--json"] },
|
|
27
33
|
{ name: "trends", shortcut: nil, desc: "Compare current vs prior period search trends", flags: ["--compare"] },
|
|
28
34
|
{ name: "devices", shortcut: nil, desc: "Desktop vs Mobile vs Tablet search traffic share", flags: ["--days"] },
|
|
29
35
|
{ name: "countries", shortcut: nil, desc: "Geographic search demand by country", flags: ["--days", "--limit"] },
|
|
@@ -52,27 +58,59 @@ COMMAND_REGISTRY = [
|
|
|
52
58
|
commands: [
|
|
53
59
|
{ name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
|
|
54
60
|
{ name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
|
|
61
|
+
{ name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--json"] },
|
|
55
62
|
{ name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
|
|
56
63
|
{ name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
|
|
57
64
|
{ name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
|
|
58
65
|
{ name: "sitemaps-submit <url>", shortcut: nil, desc: "Submit/register XML sitemap with Search Console", flags: ["-d"] },
|
|
59
66
|
{ name: "index-sitemap <file/url>", shortcut: nil, desc: "Batch notify Googlebot to index all sitemap URLs", flags: ["--delay", "--dry-run"] },
|
|
60
|
-
{ name: "inspect-sitemap <file/url>", shortcut: nil, desc: "Bulk inspect indexation status for all sitemap URLs", flags: ["--delay"] }
|
|
67
|
+
{ name: "inspect-sitemap <file/url>", shortcut: nil, desc: "Bulk inspect indexation status for all sitemap URLs", flags: ["--delay"] },
|
|
68
|
+
{ name: "indexnow <url>", shortcut: "in", desc: "Instant indexing via IndexNow protocol (Bing, Yandex, Seznam, Naver)", flags: ["--key", "--json"] },
|
|
69
|
+
{ name: "indexnow-sitemap <sitemap>", shortcut: "ins", desc: "Batch submit all sitemap URLs to IndexNow", flags: ["--key", "--limit", "--json"] },
|
|
70
|
+
{ name: "connect indexnow [key]", shortcut: nil, desc: "Configure IndexNow API key and display verification file details", flags: [] }
|
|
61
71
|
]
|
|
62
72
|
},
|
|
63
73
|
{
|
|
64
74
|
category: "Health, Diagnostics & Setup",
|
|
65
75
|
commands: [
|
|
76
|
+
{ name: "geo [url]", shortcut: "aeo", desc: "Generative Engine Optimization: Audit AI bot access & citability score (ChatGPT, Perplexity, Claude)", flags: ["--json"] },
|
|
77
|
+
{ name: "cite-sim <url|file> [query]", shortcut: "cite", desc: "AI Search Citation Simulator: Emulates ChatGPT, Perplexity & Claude citations, verifiable quote extraction & CLS score", flags: ["--query", "--model", "--csv", "--json"] },
|
|
78
|
+
{ name: "answer <query>", shortcut: "ans", desc: "Synthesize high-citability 40-60w direct answers & info-gain snippets (Featured Snippets & Perplexity)", flags: ["--brand", "--type", "--generate", "--json"] },
|
|
79
|
+
{ name: "entity [url]", shortcut: "kg", desc: "Knowledge Graph entity & sameAs disambiguation audit (Wikidata, Wikipedia, LinkedIn)", flags: ["--generate", "--json"] },
|
|
80
|
+
{ name: "serp <query|url>", shortcut: "preview", desc: "Live SERP Feature Detector (AI Overviews, Snippets, PAA, Local) & Pixel Width Simulator", flags: ["--live", "--preview", "--title", "--desc", "--url", "--json"] },
|
|
81
|
+
{ name: "serp-features <query>", shortcut: "sf", desc: "Scan live Google SERP features, Zero-Click Threat index & Rich Snippet capture playbook", flags: ["--json"] },
|
|
66
82
|
{ name: "page <url|file>", shortcut: "pg", desc: "Detailed On-Page DOM + Off-Page GSC Performance Audit", flags: ["--check-links", "--json"] },
|
|
83
|
+
{ name: "headings <url|file>", shortcut: "ht", desc: "Heading hierarchy tree validator: H1-H6 nesting, depth, empty tags & health score", flags: ["--keyword", "--json"] },
|
|
67
84
|
{ name: "site-audit [sitemap]", shortcut: "crl", desc: "Full site crawl, broken links (404/500), image alts & AI report", flags: ["--check-links", "--report", "--json"] },
|
|
85
|
+
{ name: "orphans [sitemap|url]", shortcut: "il", desc: "Internal link equity & orphan page rescue: Detect 0-link pages, click depth & suggested rescue sources", flags: ["--limit", "--concurrency", "--csv", "--json"] },
|
|
86
|
+
{ name: "speed-correlate [url]", shortcut: "cwv", desc: "Correlate Core Web Vitals (LCP/CLS) directly with Search Console impressions & projected traffic surge", flags: ["--days", "--strategy", "-d", "--json"] },
|
|
87
|
+
{ name: "firewall [url]", shortcut: "ai-bots", desc: "Scan /robots.txt & edge WAF/Cloudflare bot challenges for ChatGPT, Claude & Perplexity", flags: ["--user-agent", "--json"] },
|
|
88
|
+
{ name: "security [url]", shortcut: "sec", desc: "Security Headers & Mixed Content Auditor: HSTS preload, CSP, clickjacking & SSL/TLS certificate expiry", flags: ["--json"] },
|
|
89
|
+
{ name: "landing-roi [url]", shortcut: "roi", desc: "Landing Page Economic ROI & Revenue Hemorrhage: Merge GSC clicks with GA4 bounce rates to calculate dollar revenue leaks", flags: ["--aov", "--conv-rate", "--benchmark", "--clicks", "--bounce", "--csv", "--json"] },
|
|
90
|
+
{ name: "schema-generate [url]", shortcut: "schema-gen", desc: "Structured Data Rich Snippet Generator: Auto-synthesizes valid Schema.org JSON-LD (Product, FAQ, HowTo, Article, Software) with Google validation", flags: ["--type", "--price", "--currency", "--rating", "--format", "--open", "--json"] },
|
|
91
|
+
{ name: "sitemap-tree [path|url]", shortcut: "smt", desc: "Sitemap Index Hierarchy & Coverage Auditor: Recursive sub-sitemap tree, Google 50k URL / 50MB compliance & GSC coverage ratio", flags: ["--csv", "--json"] },
|
|
92
|
+
{ name: "titles [sitemap|url]", shortcut: "title-opt", desc: "Batch audit title pixel width (>580px) & synthesize non-truncating alternatives", flags: ["--overflow-only", "--limit", "--concurrency", "--csv", "--json"] },
|
|
93
|
+
{ name: "image-seo <url|file>", shortcut: "img", desc: "Image SEO & Next-Gen Format Auditor: WebP/AVIF formats, missing alt text, explicit width/height for CLS & LCP preloading", flags: ["--check-size", "--limit", "--csv", "--json"] },
|
|
94
|
+
{ name: "hreflang-check <url|file>", shortcut: "hreflang", desc: "International Hreflang Reciprocity Validator: Bidirectional link graph, ISO 639-1 / 3166-1 codes & x-default fallback", flags: ["--no-reciprocity", "--csv", "--json"] },
|
|
95
|
+
{ name: "eeat <url|file>", shortcut: "author", desc: "Author E-E-A-T & Credential Auditor: Person schema, credentials, editorial reviews, dates & Knowledge Graph links", flags: ["--csv", "--json"] },
|
|
68
96
|
{ name: "audit", shortcut: "a", desc: "360-degree Comprehensive SEO & GA4 health audit", flags: ["--days", "-d"] },
|
|
69
|
-
{ name: "
|
|
70
|
-
{ name: "
|
|
97
|
+
{ name: "report [domain]", shortcut: "rep", desc: "Automated Executive SEO & AI Search Report Generator: Single-file HTML dashboard & Markdown reports", flags: ["--html", "--md", "--days", "--title", "--json"] },
|
|
98
|
+
{ name: "intent-shift [domain]", shortcut: "intent", desc: "Search Intent Drift & Landing Page Mismatch Tracker: Detect transactional/informational intent divergence & compute Portfolio Volatility Index", flags: ["--days", "--brand", "--csv", "--json"] },
|
|
99
|
+
{ name: "rich-results <url|file>", shortcut: "rich-test", desc: "Google Rich Results Testing & Validation Suite: Disqualifying errors, recommended enhancements, 1-click JSON-LD fixes & browser testing", flags: ["--open", "--patch", "--csv", "--json"] },
|
|
100
|
+
{ name: "watch [domain]", shortcut: "mon", desc: "Continuous SERP Watchdog & Rank Drift Monitor: Real-time rank drop alerts, CTR crash detection, webhooks & automated cron/launchd daemon", flags: ["--once", "--daemon", "--interval", "--threshold", "--webhook", "--slack", "--cron", "--launchd", "--systemd", "--csv", "--json"] },
|
|
101
|
+
{ name: "kw-value [domain]", shortcut: "kwval", desc: "Conversion-Weighted Keyword Opportunity Matrix: Calculate revenue per keyword, simulate Top 3 pipeline upside & prioritize high-buyer-intent terms", flags: ["--aov", "--conv-rate", "--margin", "--target-pos", "--days", "--csv", "--json"] },
|
|
102
|
+
{ name: "mobile-parity [domain]", shortcut: "mobile", desc: "Mobile vs. Desktop SERP Parity Auditor: Cross-device rank gaps, responsive suppression penalties & mobile CTR loss diagnosis", flags: ["--days", "--min-imp", "--gap-threshold", "--csv", "--json"] },
|
|
103
|
+
{ name: "aio-hunter [query|domain]", shortcut: "aio", desc: "Google AI Overview (AIO) Opportunity Hunter: Detects AIO SERP presence, extracts cited sources, quantifies citation gaps & generates snippet capture recipes", flags: ["--limit", "--min-imp", "--days", "--csv", "--json"] },
|
|
104
|
+
{ name: "skill-pack [dir]", shortcut: "agent-pack", desc: "Autonomous AI Agent Skill Packaging Engine: Synthesizes production SKILL.md, recipes.json & integration hooks for Antigravity, Claude Code & Cursor", flags: ["--target", "--dir", "--dry-run", "--json"] },
|
|
105
|
+
{ name: "zombies [sitemap|file]", shortcut: "z", desc: "Zombie Content Purger & Crawl Equity Optimizer: CEDI score (0-100), automated 410/301 triage & 1-click server rules", flags: ["--days", "--threshold", "--action", "--format", "--inventory", "--csv", "--json"] },
|
|
106
|
+
{ name: "soft-404 [url|file]", shortcut: "404", desc: "404 & Soft-404 Crawl Error Diagnostic: Detects thin/empty pages returning 200 OK, calculates crawl waste & synthesizes smart 301/410 redirect rules", flags: ["--concurrency", "--threshold", "--format", "--csv", "--json"] },
|
|
107
|
+
{ name: "switch <domain>", shortcut: "use", desc: "Instant agency domain & credentials switcher (domain, alias, or index #)", flags: ["--json"] },
|
|
108
|
+
{ name: "vault [action]", shortcut: nil, desc: "Agency Credential Vault: AES-256-GCM encrypted store for 50+ service accounts (list, add, remove, status)", flags: ["--domain", "--alias", "--property", "--json"] },
|
|
71
109
|
{ name: "domains", shortcut: nil, desc: "List verified domains and GA4 property links", flags: [] },
|
|
110
|
+
{ name: "doctor", shortcut: "doc", desc: "Zero-Dependency Architecture Certification & System Health: Verifies 100% pure Ruby stdlib, cold-start latency (<100ms), OpenSSL cipher support & config permissions", flags: ["--strict", "--fix", "--json"] },
|
|
72
111
|
{ name: "where", shortcut: nil, desc: "Inspect installation path, active key, and config file", flags: [] },
|
|
73
112
|
{ name: "connect", shortcut: nil, desc: "1-Click Setup Wizard: auto-detects key or drag & drop", flags: [] },
|
|
74
113
|
{ name: "connect-ga4", shortcut: nil, desc: "Interactive GA4 linking wizard", flags: [] },
|
|
75
|
-
{ name: "open", shortcut: nil, desc: "Reveal configuration directory (~/.config/gsc) in Finder", flags: [] },
|
|
76
114
|
{ name: "prompts [id]", shortcut: "pb", desc: "25 Autonomous AI SEO Playbooks & ready-to-paste prompts", flags: ["--copy", "--json"] },
|
|
77
115
|
{ name: "skills [install|show]", shortcut: nil, desc: "Inspect or auto-install AI Agent Skill", flags: [] },
|
|
78
116
|
{ name: "commands", shortcut: nil, desc: "List all commands (human-readable or JSON with --json)", flags: ["--json"] }
|
|
@@ -267,7 +305,7 @@ COMMAND_REGISTRY = [
|
|
|
267
305
|
2. **Optimal Length**: Keep `<title>` between **50 and 60 characters** (maximum 580px width) so Google does not truncate with `...`.
|
|
268
306
|
3. **Emotional Hook / CTR Multiplier**: Include brackets `[Free Calculator]`, actionable numbers (`10 Best`, `2026 Checklist`), or primary value props.
|
|
269
307
|
4. **Brand Suffix**: Always append ` | BrandName` at the end.
|
|
270
|
-
5. **Meta Description**: 130–155 characters summarizing the page benefit with a clear call-to-action (e.g. *"
|
|
308
|
+
5. **Meta Description**: 130–155 characters summarizing the page benefit with a clear call-to-action (e.g. *"Inspect technical SEO health, Core Web Vitals, and index coverage instantly. Free diagnostic audit."*).
|
|
271
309
|
6. **Heading Polish**: Ensure `<h1>` matches search intent and **never ends with a period (`.`)**.
|
|
272
310
|
|
|
273
311
|
#### Step 4: Immediately Trigger Googlebot Indexing
|
data/lib/gsc/config.rb
CHANGED
|
@@ -9,6 +9,15 @@ module GSC
|
|
|
9
9
|
CONFIG_DIR = File.expand_path('~/.config/gsc')
|
|
10
10
|
CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
|
|
11
11
|
|
|
12
|
+
def self.get(key)
|
|
13
|
+
load[key.to_s]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.set(key, value)
|
|
17
|
+
save(key.to_s => value)
|
|
18
|
+
value
|
|
19
|
+
end
|
|
20
|
+
|
|
12
21
|
def self.load
|
|
13
22
|
return {} unless File.exist?(CONFIG_FILE)
|
|
14
23
|
JSON.parse(File.read(CONFIG_FILE))
|
|
@@ -144,7 +153,7 @@ def self.load_saved_keywords(domain, identifier)
|
|
|
144
153
|
|
|
145
154
|
target_file = if identifier.to_s =~ /^\d+$/
|
|
146
155
|
idx = identifier.to_i - 1
|
|
147
|
-
list[idx]&.fetch('path', nil)
|
|
156
|
+
idx >= 0 ? list[idx]&.fetch('path', nil) : nil
|
|
148
157
|
else
|
|
149
158
|
found = list.find { |item| item['file'] == identifier || item['seed'] == identifier || item['file'].include?(identifier.to_s) }
|
|
150
159
|
found ? found['path'] : nil
|
|
@@ -162,7 +171,7 @@ def self.delete_saved_keywords(domain, identifier)
|
|
|
162
171
|
|
|
163
172
|
target_file = if identifier.to_s =~ /^\d+$/
|
|
164
173
|
idx = identifier.to_i - 1
|
|
165
|
-
list[idx]&.fetch('path', nil)
|
|
174
|
+
idx >= 0 ? list[idx]&.fetch('path', nil) : nil
|
|
166
175
|
else
|
|
167
176
|
found = list.find { |item| item['file'] == identifier || item['file'].include?(identifier.to_s) }
|
|
168
177
|
found ? found['path'] : nil
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'set'
|
|
5
|
+
|
|
6
|
+
module GSC
|
|
7
|
+
class ContentGap
|
|
8
|
+
STOP_WORDS = Set.new(%w[
|
|
9
|
+
a about above after again against all am an and any are aren't as at be because been before being below
|
|
10
|
+
between both but by can't cannot could couldn't did didn't do does doesn't doing don't down during each
|
|
11
|
+
few for from further had hadn't has hasn't have haven't having he he'd he'll he's her here here's hers
|
|
12
|
+
herself him himself his how how's i i'd i'll i'm i've if in into is isn't it it's its itself let's me
|
|
13
|
+
more most mustn't my myself no nor not of off on once only or other ought our ours ourselves out over own
|
|
14
|
+
same shan't she she'd she'll she's should shouldn't so some such than that that's the their theirs them
|
|
15
|
+
themselves then there there's these they they'd they'll they're they've this those through to too under until
|
|
16
|
+
up very was wasn't we we'd we'll we're we've were weren't what what's when when's where where's which while
|
|
17
|
+
who who's whom why why's with won't would wouldn't you you'd you'll you're you've your yours yourself yourselves
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
attr_reader :url1, :url2
|
|
21
|
+
|
|
22
|
+
def initialize(url1, url2)
|
|
23
|
+
@url1 = url1
|
|
24
|
+
@url2 = url2
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def analyze
|
|
28
|
+
pa1 = GSC::PageAnalyzer.new(@url1)
|
|
29
|
+
pa2 = GSC::PageAnalyzer.new(@url2)
|
|
30
|
+
|
|
31
|
+
data1 = pa1.fetch_and_analyze
|
|
32
|
+
data2 = pa2.fetch_and_analyze
|
|
33
|
+
|
|
34
|
+
text1 = extract_clean_text(pa1.html || '')
|
|
35
|
+
text2 = extract_clean_text(pa2.html || '')
|
|
36
|
+
|
|
37
|
+
tokens1 = tokenize(text1)
|
|
38
|
+
tokens2 = tokenize(text2)
|
|
39
|
+
|
|
40
|
+
unigrams1 = ngrams(tokens1, 1)
|
|
41
|
+
unigrams2 = ngrams(tokens2, 1)
|
|
42
|
+
|
|
43
|
+
bigrams1 = ngrams(tokens1, 2)
|
|
44
|
+
bigrams2 = ngrams(tokens2, 2)
|
|
45
|
+
|
|
46
|
+
trigrams1 = ngrams(tokens1, 3)
|
|
47
|
+
trigrams2 = ngrams(tokens2, 3)
|
|
48
|
+
|
|
49
|
+
# Competitor terms that occur at least 2 times, but occur 0 times in page1
|
|
50
|
+
missing_unigrams = term_gap(unigrams1, unigrams2, min_count: 2)
|
|
51
|
+
missing_bigrams = term_gap(bigrams1, bigrams2, min_count: 2)
|
|
52
|
+
missing_trigrams = term_gap(trigrams1, trigrams2, min_count: 2)
|
|
53
|
+
|
|
54
|
+
# Heading gaps (H2/H3 in page2 that have no match in page1)
|
|
55
|
+
h1 = (data1.dig(:headings, :h2) || []) + (data1.dig(:headings, :h3) || [])
|
|
56
|
+
h2 = (data2.dig(:headings, :h2) || []) + (data2.dig(:headings, :h3) || [])
|
|
57
|
+
|
|
58
|
+
missing_headings = h2.reject do |heading|
|
|
59
|
+
h1.any? { |my_h| my_h.downcase.include?(heading.downcase[0..15]) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
page1: { url: @url1, word_count: tokens1.length },
|
|
64
|
+
page2: { url: @url2, word_count: tokens2.length },
|
|
65
|
+
missing_unigrams: missing_unigrams.first(15),
|
|
66
|
+
missing_bigrams: missing_bigrams.first(15),
|
|
67
|
+
missing_trigrams: missing_trigrams.first(10),
|
|
68
|
+
missing_headings: missing_headings.first(10)
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def extract_clean_text(html)
|
|
75
|
+
text = html.dup
|
|
76
|
+
text.gsub!(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/i, ' ')
|
|
77
|
+
text.gsub!(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/i, ' ')
|
|
78
|
+
text.gsub!(/<nav\b[^<]*(?:(?!<\/nav>)<[^<]*)*<\/nav>/i, ' ')
|
|
79
|
+
text.gsub!(/<footer\b[^<]*(?:(?!<\/footer>)<[^<]*)*<\/footer>/i, ' ')
|
|
80
|
+
text.gsub!(/<[^>]+>/, ' ')
|
|
81
|
+
text.gsub!(/&[a-z]+;/i, ' ')
|
|
82
|
+
text.gsub!(/\s+/, ' ')
|
|
83
|
+
text.strip
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def tokenize(text)
|
|
87
|
+
text.downcase.scan(/[a-z0-9]+/).reject { |w| w.length < 3 || STOP_WORDS.include?(w) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def ngrams(tokens, n)
|
|
91
|
+
counts = Hash.new(0)
|
|
92
|
+
return counts if tokens.length < n
|
|
93
|
+
|
|
94
|
+
(0..(tokens.length - n)).each do |i|
|
|
95
|
+
gram = tokens[i, n].join(' ')
|
|
96
|
+
counts[gram] += 1
|
|
97
|
+
end
|
|
98
|
+
counts
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def term_gap(counts1, counts2, min_count: 2)
|
|
102
|
+
gap = []
|
|
103
|
+
counts2.each do |term, count2|
|
|
104
|
+
count1 = counts1[term] || 0
|
|
105
|
+
if count2 >= min_count && count1 == 0
|
|
106
|
+
gap << { term: term, competitor_count: count2, your_count: count1 }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
gap.sort_by { |item| -item[:competitor_count] }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GSC
|
|
4
|
+
class CtrCurve
|
|
5
|
+
# Empirical Google Organic SERP CTR Curve benchmark (Ahrefs / Backlinko / Advanced Web Ranking composite)
|
|
6
|
+
BENCHMARK_CTR = {
|
|
7
|
+
1 => 28.0,
|
|
8
|
+
2 => 15.5,
|
|
9
|
+
3 => 11.0,
|
|
10
|
+
4 => 8.0,
|
|
11
|
+
5 => 6.0,
|
|
12
|
+
6 => 4.5,
|
|
13
|
+
7 => 3.5,
|
|
14
|
+
8 => 2.8,
|
|
15
|
+
9 => 2.2,
|
|
16
|
+
10 => 1.8,
|
|
17
|
+
11 => 1.4,
|
|
18
|
+
12 => 1.2,
|
|
19
|
+
13 => 1.0,
|
|
20
|
+
14 => 0.9,
|
|
21
|
+
15 => 0.8,
|
|
22
|
+
16 => 0.7,
|
|
23
|
+
17 => 0.6,
|
|
24
|
+
18 => 0.5,
|
|
25
|
+
19 => 0.4,
|
|
26
|
+
20 => 0.4
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
DEFAULT_PAGE2_CTR = 0.5
|
|
30
|
+
DEFAULT_PAGE3_CTR = 0.2
|
|
31
|
+
|
|
32
|
+
def self.benchmark_for(position)
|
|
33
|
+
pos = position.to_f.round
|
|
34
|
+
return BENCHMARK_CTR[1] if pos <= 1
|
|
35
|
+
return BENCHMARK_CTR[pos] if BENCHMARK_CTR.key?(pos)
|
|
36
|
+
return DEFAULT_PAGE2_CTR if pos <= 20
|
|
37
|
+
|
|
38
|
+
DEFAULT_PAGE3_CTR
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.project(rows, target_pos: 3, min_imp: 10)
|
|
42
|
+
target_pos = target_pos.to_i
|
|
43
|
+
target_pos = 3 if target_pos <= 0
|
|
44
|
+
target_ctr = benchmark_for(target_pos)
|
|
45
|
+
|
|
46
|
+
analyzed = []
|
|
47
|
+
total_incremental = 0
|
|
48
|
+
underperforming_queries = []
|
|
49
|
+
|
|
50
|
+
rows.each do |row|
|
|
51
|
+
imp = row[:impressions] || row['impressions'] || 0
|
|
52
|
+
next if imp < min_imp
|
|
53
|
+
|
|
54
|
+
clicks = row[:clicks] || row['clicks'] || 0
|
|
55
|
+
actual_ctr = if imp > 0
|
|
56
|
+
(clicks.to_f / imp * 100.0).round(2)
|
|
57
|
+
elsif (row[:ctr] || row['ctr'])
|
|
58
|
+
(row[:ctr] || row['ctr']).to_f.round(2)
|
|
59
|
+
else
|
|
60
|
+
0.0
|
|
61
|
+
end
|
|
62
|
+
pos = (row[:position] || row['position'] || 100.0).to_f.round(1)
|
|
63
|
+
expected_ctr = benchmark_for(pos)
|
|
64
|
+
|
|
65
|
+
# Gain projection if ranking moved to target_pos
|
|
66
|
+
projected_clicks = ((target_ctr / 100.0) * imp).round
|
|
67
|
+
incremental_clicks = [projected_clicks - clicks, 0].max
|
|
68
|
+
|
|
69
|
+
# Priority score: Higher incremental clicks and closer to top page 1 = higher leverage
|
|
70
|
+
proximity_factor = [1.0, (25.0 - [pos, 25.0].min) / 20.0].max
|
|
71
|
+
opportunity_score = (incremental_clicks * proximity_factor).round(1)
|
|
72
|
+
|
|
73
|
+
# Flag title tag optimization wins: ranking on Page 1 (<=10) but CTR is < 50% of benchmark
|
|
74
|
+
is_underperforming = pos <= 10.0 && actual_ctr < (expected_ctr * 0.6) && imp >= 20
|
|
75
|
+
|
|
76
|
+
data = {
|
|
77
|
+
query: row[:query] || row['query'],
|
|
78
|
+
current_position: pos,
|
|
79
|
+
target_position: target_pos,
|
|
80
|
+
impressions: imp,
|
|
81
|
+
current_clicks: clicks,
|
|
82
|
+
current_ctr: actual_ctr,
|
|
83
|
+
expected_ctr: expected_ctr,
|
|
84
|
+
target_ctr: target_ctr,
|
|
85
|
+
projected_clicks: projected_clicks,
|
|
86
|
+
simulated_clicks: projected_clicks, # alias for backward compatibility
|
|
87
|
+
incremental_clicks: incremental_clicks,
|
|
88
|
+
opportunity_score: opportunity_score,
|
|
89
|
+
underperforming: is_underperforming
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
analyzed << data
|
|
93
|
+
total_incremental += incremental_clicks
|
|
94
|
+
underperforming_queries << data if is_underperforming
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Sort by highest opportunity score
|
|
98
|
+
analyzed.sort_by! { |r| -r[:opportunity_score] }
|
|
99
|
+
underperforming_queries.sort_by! { |r| -r[:impressions] }
|
|
100
|
+
|
|
101
|
+
{
|
|
102
|
+
target_position: target_pos,
|
|
103
|
+
target_ctr: target_ctr,
|
|
104
|
+
total_queries_analyzed: analyzed.size,
|
|
105
|
+
total_incremental_clicks: total_incremental,
|
|
106
|
+
opportunities: analyzed,
|
|
107
|
+
underperformers: underperforming_queries
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class << self
|
|
112
|
+
alias simulate project
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|