gsc-cli 2.1.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.
- checksums.yaml +4 -4
- data/README.md +410 -414
- data/bin/gsc +28067 -5661
- data/dist/gsc +29121 -5046
- 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/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 +707 -5265
- data/lib/gsc/cli_advanced.rb +987 -44
- 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 +2 -2
- 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_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 +153 -36
- data/lib/gsc/keyword_value.rb +191 -0
- data/lib/gsc/landing_roi.rb +195 -0
- data/lib/gsc/llms_generator.rb +343 -22
- data/lib/gsc/low_ctr_rewriter.rb +408 -0
- data/lib/gsc/mobile_parity.rb +222 -0
- data/lib/gsc/network_tracer.rb +8 -1
- data/lib/gsc/page_analyzer.rb +47 -7
- 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 +46 -15
- data/lib/gsc/schema_generator.rb +788 -0
- data/lib/gsc/schema_validator.rb +36 -38
- 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 +108 -22
- 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 +118 -0
- metadata +75 -1
data/lib/gsc/cli_advanced.rb
CHANGED
|
@@ -10,7 +10,7 @@ module GSC
|
|
|
10
10
|
def self.handle_suggest_command(target, options)
|
|
11
11
|
query = target.to_s.strip
|
|
12
12
|
if query.empty?
|
|
13
|
-
puts Color.c("ā Error: Query required. Example: gsc suggest \"
|
|
13
|
+
puts Color.c("ā Error: Query required. Example: gsc suggest \"seo audit\"", Color::RED)
|
|
14
14
|
return
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -52,7 +52,7 @@ module GSC
|
|
|
52
52
|
def self.handle_questions_command(target, options)
|
|
53
53
|
query = target.to_s.strip
|
|
54
54
|
if query.empty?
|
|
55
|
-
puts Color.c("ā Error: Query required. Example: gsc questions \"
|
|
55
|
+
puts Color.c("ā Error: Query required. Example: gsc questions \"seo audit\"", Color::RED)
|
|
56
56
|
return
|
|
57
57
|
end
|
|
58
58
|
|
|
@@ -91,7 +91,7 @@ module GSC
|
|
|
91
91
|
domains = domains.compact
|
|
92
92
|
|
|
93
93
|
if domains.empty?
|
|
94
|
-
puts Color.c("ā Error: Domain required. Example: gsc authority
|
|
94
|
+
puts Color.c("ā Error: Domain required. Example: gsc authority example.com", Color::RED)
|
|
95
95
|
return
|
|
96
96
|
end
|
|
97
97
|
|
|
@@ -136,8 +136,12 @@ module GSC
|
|
|
136
136
|
|
|
137
137
|
# 4. PageSpeed Core Web Vitals
|
|
138
138
|
def self.handle_speed_command(target, options)
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
raw = target || (Config.default_domain ? "https://#{Config.default_domain}" : nil)
|
|
140
|
+
if raw.nil? || raw.empty?
|
|
141
|
+
puts Color.c("ā Error: Target URL or domain required. Example: gsc speed https://mybrand.com", Color::RED)
|
|
142
|
+
return
|
|
143
|
+
end
|
|
144
|
+
url = raw.to_s.start_with?('http') ? raw : "https://#{raw}"
|
|
141
145
|
strategy = options[:strategy] || 'mobile'
|
|
142
146
|
|
|
143
147
|
puts BANNER unless options[:json] || options[:in_dashboard]
|
|
@@ -183,6 +187,90 @@ module GSC
|
|
|
183
187
|
puts ""
|
|
184
188
|
end
|
|
185
189
|
|
|
190
|
+
# 4b. Core Web Vitals & GSC Performance Correlator
|
|
191
|
+
def self.handle_speed_correlate_command(target, options)
|
|
192
|
+
domain = options[:domain] || Config.default_domain
|
|
193
|
+
if (domain.nil? || domain.empty?) && (target.nil? || target.empty?)
|
|
194
|
+
puts Color.c("ā Error: Target URL or domain required. Example: gsc speed-correlate https://mybrand.com", Color::RED)
|
|
195
|
+
return
|
|
196
|
+
end
|
|
197
|
+
url = target || "https://#{domain}/"
|
|
198
|
+
url = "https://#{url}" unless url =~ %r{^https?://}
|
|
199
|
+
strategy = options[:strategy] || 'mobile'
|
|
200
|
+
days = (options[:days] || 28).to_i
|
|
201
|
+
|
|
202
|
+
# Attempt to get GSC API client if authenticated
|
|
203
|
+
api = nil
|
|
204
|
+
sa = GSC::Auth.find_service_account(options[:key], domain)
|
|
205
|
+
if sa && sa[:data]
|
|
206
|
+
token = GSC::Auth.fetch_access_token(sa[:data])
|
|
207
|
+
client = GSC::Client.new(token: token)
|
|
208
|
+
api = GSC::API.new(client)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
correlator = GSC::SpeedCorrelator.new(url, api: api, domain: domain, days: days, strategy: strategy, options: options)
|
|
212
|
+
data = correlator.correlate
|
|
213
|
+
|
|
214
|
+
if options[:json]
|
|
215
|
+
puts JSON.pretty_generate(data)
|
|
216
|
+
return
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
puts BANNER unless options[:in_dashboard]
|
|
220
|
+
puts "ā” #{Color::BOLD}CORE WEB VITALS & SEARCH CONSOLE PERFORMANCE CORRELATOR:#{Color::RESET}"
|
|
221
|
+
puts " Target URL: #{Color.c(url, Color::CYAN, Color::BOLD)} [Strategy: #{strategy.upcase}]"
|
|
222
|
+
puts "ā" * 80
|
|
223
|
+
|
|
224
|
+
cwv = data[:core_web_vitals] || {}
|
|
225
|
+
gsc = data[:gsc_performance] || {}
|
|
226
|
+
diag = data[:correlation_diagnosis] || {}
|
|
227
|
+
proj = data[:projections] || {}
|
|
228
|
+
|
|
229
|
+
perf_score = cwv[:performance_score] || 0
|
|
230
|
+
score_color = perf_score >= 90 ? Color::GREEN : (perf_score >= 50 ? Color::YELLOW : Color::RED)
|
|
231
|
+
|
|
232
|
+
puts "š± #{Color::BOLD}LIGHTHOUSE PERFORMANCE:#{Color::RESET} #{Color.c("#{perf_score}/100", score_color, Color::BOLD)} [Source: #{cwv[:source]}]"
|
|
233
|
+
puts " ⢠LCP (Largest Contentful Paint) : #{Color.c(cwv[:lcp_display].to_s, Color::BOLD)} [Status: #{diag[:lcp_status]}]"
|
|
234
|
+
puts " ⢠CLS (Cumulative Layout Shift) : #{Color.c(cwv[:cls_display].to_s, Color::BOLD)} [Status: #{diag[:cls_status]}]"
|
|
235
|
+
puts " ⢠FCP (First Contentful Paint) : #{Color.c(cwv[:fcp_display].to_s, Color::BOLD)}"
|
|
236
|
+
puts " ⢠TBT (Total Blocking Time) : #{Color.c(cwv[:tbt_display].to_s, Color::BOLD)}"
|
|
237
|
+
|
|
238
|
+
puts "\nš #{Color::BOLD}SEARCH CONSOLE TRACTION (Past #{days} Days):#{Color::RESET}"
|
|
239
|
+
puts " ⢠Impressions : #{Color.c(gsc[:impressions].to_s, Color::CYAN, Color::BOLD)}"
|
|
240
|
+
puts " ⢠Clicks : #{Color.c(gsc[:clicks].to_s, Color::GREEN, Color::BOLD)}"
|
|
241
|
+
puts " ⢠CTR : #{Color.c("#{gsc[:ctr]}%", Color::BOLD)}"
|
|
242
|
+
puts " ⢠Avg Position: #{Color.c(gsc[:position].to_s, Color::BOLD)}"
|
|
243
|
+
|
|
244
|
+
puts "\nš§ #{Color::BOLD}ALGORITHMIC CWV IMPACT DIAGNOSIS:#{Color::RESET}"
|
|
245
|
+
diag_color = diag[:overall_cwv_pass] ? Color::GREEN : Color::YELLOW
|
|
246
|
+
puts " ⢠CWV Overall Verdict : [#{Color.c(diag[:overall_cwv_pass] ? 'PASSED' : 'NEEDS OPTIMIZATION', diag_color, Color::BOLD)}]"
|
|
247
|
+
puts " ⢠Algorithmic Drag : #{Color.c(diag[:algorithmic_status], Color::BOLD)}"
|
|
248
|
+
|
|
249
|
+
puts "\nš #{Color::BOLD}PROJECTED SEARCH IMPRESSION & TRAFFIC SURGE (Upon Reaching 'Good' CWV):#{Color::RESET}"
|
|
250
|
+
puts " ⢠Potential Impression Lift : #{Color.c("+#{proj[:potential_lift_percentage]}%", Color::GREEN, Color::BOLD)} (+#{proj[:incremental_impressions_gain]} impressions)"
|
|
251
|
+
puts " ⢠Projected Impressions Target: #{Color.c(proj[:projected_impressions].to_s, Color::CYAN, Color::BOLD)}"
|
|
252
|
+
puts " ⢠Projected Rank Lift : #{Color.c(proj[:estimated_position_improvement].to_s, Color::GREEN, Color::BOLD)} (Pos #{proj[:current_position]} -> Pos #{proj[:projected_position]})"
|
|
253
|
+
puts " ⢠Projected Monthly Click Gain: #{Color.c("+#{proj[:projected_incremental_monthly_clicks]} clicks/mo", Color::GREEN, Color::BOLD)}"
|
|
254
|
+
|
|
255
|
+
action_plan = data[:action_plan] || []
|
|
256
|
+
if action_plan.any?
|
|
257
|
+
puts "\nš ļø #{Color::BOLD}PRIORITIZED ENGINEERING FIXES FOR SPEED & RANKINGS:#{Color::RESET}"
|
|
258
|
+
action_plan.each_with_index do |act, idx|
|
|
259
|
+
puts " #{idx + 1}. [#{Color.c(act[:priority], Color::BOLD)}] [#{act[:metric]}]: #{act[:recommendation]}"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
opps = cwv[:opportunities] || []
|
|
264
|
+
if opps.any?
|
|
265
|
+
puts "\nš” #{Color::BOLD}IDENTIFIED BOTTLENECK OPPORTUNITIES:#{Color::RESET}"
|
|
266
|
+
opps.each do |o|
|
|
267
|
+
puts " ⢠#{o[:title]} (#{Color.c(o[:display] || "#{o[:savings_ms]}ms savings", Color::YELLOW)})"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
puts "\n" + ("ā" * 80) + "\n"
|
|
272
|
+
end
|
|
273
|
+
|
|
186
274
|
# 5. Page Comparison
|
|
187
275
|
def self.handle_compare_command(target, extra, options)
|
|
188
276
|
url1 = target
|
|
@@ -299,13 +387,19 @@ module GSC
|
|
|
299
387
|
puts ""
|
|
300
388
|
end
|
|
301
389
|
|
|
302
|
-
# 7. Internal Links Audit
|
|
390
|
+
# 7. Internal Links Audit & Orphan Page Rescue Engine
|
|
303
391
|
def self.handle_internal_links_command(target, options)
|
|
304
|
-
|
|
305
|
-
|
|
392
|
+
raw = target || (Config.default_domain ? "https://#{Config.default_domain}" : nil)
|
|
393
|
+
if raw.nil? || raw.empty?
|
|
394
|
+
puts Color.c("ā Error: Target URL or domain required. Example: gsc internal-links https://mybrand.com", Color::RED)
|
|
395
|
+
return
|
|
396
|
+
end
|
|
397
|
+
base_url = raw.to_s.start_with?('http') ? raw : "https://#{raw}"
|
|
398
|
+
conc = options[:concurrency] || 5
|
|
399
|
+
il = GSC::InternalLinks.new(base_url, limit: options[:limit] || 50, concurrency: conc)
|
|
306
400
|
|
|
307
401
|
puts BANNER unless options[:json] || options[:in_dashboard]
|
|
308
|
-
puts "šøļø Auditing Internal Links & Orphan Pages for: #{Color.c(base_url, Color::CYAN)}...\n" unless options[:json]
|
|
402
|
+
puts "šøļø Auditing Internal Links & Orphan Pages for: #{Color.c(base_url, Color::CYAN)} (#{conc} worker threads)...\n" unless options[:json]
|
|
309
403
|
|
|
310
404
|
data = il.audit
|
|
311
405
|
|
|
@@ -314,25 +408,66 @@ module GSC
|
|
|
314
408
|
return
|
|
315
409
|
end
|
|
316
410
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
411
|
+
score_color = case data[:health_score]
|
|
412
|
+
when 85..100 then Color::GREEN
|
|
413
|
+
when 70..84 then Color::YELLOW
|
|
414
|
+
else Color::RED
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
puts "š Domain: #{Color.c(data[:base_url], Color::CYAN, Color::BOLD)}"
|
|
418
|
+
puts "ā” Crawl Performance: #{data[:total_pages]} pages mapped in #{data[:crawl_duration_s]}s"
|
|
419
|
+
puts "š Link Equity Score: #{Color.c("#{data[:health_score]}/100 (Grade #{data[:health_grade]})", score_color, Color::BOLD)}"
|
|
420
|
+
puts "šØ True Orphans: #{Color.c(data[:orphans].length.to_s, data[:orphans].empty? ? Color::GREEN : Color::RED, Color::BOLD)} (0 incoming internal links)"
|
|
421
|
+
puts "ā ļø Weakly Linked: #{Color.c(data[:weak_pages].length.to_s, Color::YELLOW, Color::BOLD)} (Only 1 incoming internal link)"
|
|
422
|
+
puts "š§ Buried Pages: #{Color.c(data[:deep_pages].length.to_s, data[:deep_pages].empty? ? Color::GREEN : Color::YELLOW, Color::BOLD)} (Click depth >= 4)"
|
|
321
423
|
puts "ā" * 80
|
|
322
424
|
|
|
323
|
-
|
|
324
|
-
unless
|
|
325
|
-
puts "\n#{Color::BOLD}šØ ORPHAN
|
|
326
|
-
|
|
327
|
-
puts "
|
|
425
|
+
orphan_details = data[:orphan_details] || []
|
|
426
|
+
unless orphan_details.empty?
|
|
427
|
+
puts "\n#{Color::BOLD}šØ ORPHAN PAGE RESCUE PLAYBOOK (#{orphan_details.size} crawl dead-ends):#{Color::RESET}"
|
|
428
|
+
orphan_details.each_with_index do |orp, idx|
|
|
429
|
+
puts "\n #{Color::BOLD}#{idx + 1}. #{Color.c(orp[:url], Color::RED, Color::BOLD)}#{Color::RESET} [Depth: #{orp[:depth]}]"
|
|
430
|
+
puts " #{Color::BOLD}Rescue Prescriptions:#{Color::RESET}"
|
|
431
|
+
orp[:suggested_rescues].each do |rec|
|
|
432
|
+
puts " ⢠#{Color.c(rec[:action], Color::CYAN)}"
|
|
433
|
+
end
|
|
328
434
|
end
|
|
435
|
+
else
|
|
436
|
+
puts "\n#{Color.c('ā
EXCELLENT! No orphan pages detected on site.', Color::GREEN, Color::BOLD)}"
|
|
329
437
|
end
|
|
330
438
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
puts "
|
|
439
|
+
weak = data[:weak_pages] || []
|
|
440
|
+
unless weak.empty?
|
|
441
|
+
puts "\n#{Color::BOLD}ā ļø WEAKLY LINKED PAGES (Vulnerable to De-indexation):#{Color::RESET}"
|
|
442
|
+
weak.first(5).each do |w|
|
|
443
|
+
puts " ⢠#{w[:url]} (Linked only from: #{Color.c(w[:source].to_s, Color::GRAY)})"
|
|
444
|
+
end
|
|
445
|
+
puts " ... and #{weak.size - 5} more weak pages" if weak.size > 5
|
|
334
446
|
end
|
|
335
|
-
|
|
447
|
+
|
|
448
|
+
deep = data[:deep_pages] || []
|
|
449
|
+
unless deep.empty?
|
|
450
|
+
puts "\n#{Color::BOLD}š§ BURIED PAGES (Crawl Depth >= 4 clicks):#{Color::RESET}"
|
|
451
|
+
deep.first(5).each do |dp|
|
|
452
|
+
puts " ⢠#{dp[:url]} (#{Color.c("#{dp[:depth]} clicks deep", Color::YELLOW)})"
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
puts "\n#{Color::BOLD}š TOP LINK EQUITY HUBS (High-Authority Internal Sources):#{Color::RESET}"
|
|
457
|
+
(data[:top_linked] || []).first(6).each do |top|
|
|
458
|
+
title_str = top[:title].to_s.empty? ? '' : " ā \"#{top[:title][0..35]}...\""
|
|
459
|
+
puts " ⢠#{Color.c(top[:url], Color::BOLD)}#{title_str} (#{Color.c(top[:incoming_count].to_s, Color::CYAN)} in / #{top[:outgoing_count]} out)"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
if options[:csv]
|
|
463
|
+
csv_data = orphan_details.map do |o|
|
|
464
|
+
[o[:url], o[:depth], o[:suggested_rescues].map { |r| r[:source_url] }.join('; ')]
|
|
465
|
+
end
|
|
466
|
+
write_csv(options[:csv], %w[OrphanURL Depth RecommendedRescueSources], csv_data)
|
|
467
|
+
puts Color.c("\nš Exported orphan rescue playbook to #{options[:csv]}", Color::CYAN)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
puts "\n" + ("ā" * 80) + "\n"
|
|
336
471
|
end
|
|
337
472
|
|
|
338
473
|
# 8. Schema Validator & Generator
|
|
@@ -351,7 +486,12 @@ module GSC
|
|
|
351
486
|
return
|
|
352
487
|
end
|
|
353
488
|
|
|
354
|
-
|
|
489
|
+
raw = target || (Config.default_domain ? "https://#{Config.default_domain}" : nil)
|
|
490
|
+
if raw.nil? || raw.empty?
|
|
491
|
+
puts Color.c("ā Error: Target URL or domain required. Example: gsc schema https://mybrand.com", Color::RED)
|
|
492
|
+
return
|
|
493
|
+
end
|
|
494
|
+
url = raw.to_s.start_with?('http') ? raw : "https://#{raw}"
|
|
355
495
|
sv = GSC::SchemaValidator.new(url)
|
|
356
496
|
data = sv.audit
|
|
357
497
|
|
|
@@ -381,7 +521,12 @@ module GSC
|
|
|
381
521
|
|
|
382
522
|
# 9. LLMS.txt & AI Search
|
|
383
523
|
def self.handle_llms_command(target, extra, options)
|
|
384
|
-
|
|
524
|
+
raw = target || (Config.default_domain ? "https://#{Config.default_domain}" : nil)
|
|
525
|
+
if raw.nil? || raw.empty?
|
|
526
|
+
puts Color.c("ā Error: Target URL or domain required. Example: gsc llms https://mybrand.com", Color::RED)
|
|
527
|
+
return
|
|
528
|
+
end
|
|
529
|
+
base_url = raw.to_s.start_with?('http') ? raw : "https://#{raw}"
|
|
385
530
|
llms = GSC::LlmsGenerator.new(base_url)
|
|
386
531
|
|
|
387
532
|
if extra == 'audit' || options[:audit]
|
|
@@ -415,10 +560,27 @@ module GSC
|
|
|
415
560
|
end
|
|
416
561
|
end
|
|
417
562
|
|
|
418
|
-
# 10. SERP & Social Preview
|
|
563
|
+
# 10. SERP & Social Preview / Live SERP Feature Detector
|
|
419
564
|
def self.handle_preview_command(target, options)
|
|
420
|
-
|
|
421
|
-
|
|
565
|
+
is_url = target.to_s.start_with?('http://', 'https://')
|
|
566
|
+
has_title_or_url = is_url || options[:url] || options[:title] || options[:desc] || options[:description] || options[:preview] || (target.to_s =~ /[:|ā]/)
|
|
567
|
+
|
|
568
|
+
if options[:live] || options[:features] || (!has_title_or_url && !target.to_s.empty?)
|
|
569
|
+
return handle_serp_features_command(target, options)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
url = options[:url] || (target if is_url)
|
|
573
|
+
url ||= (Config.default_domain ? "https://#{Config.default_domain}" : nil)
|
|
574
|
+
title = options[:title] || (!is_url && !target.to_s.empty? ? target.to_s : nil)
|
|
575
|
+
desc = options[:desc] || options[:description]
|
|
576
|
+
|
|
577
|
+
if url.nil? && (title.nil? || title.empty?)
|
|
578
|
+
puts Color.c("ā Error: Target URL or title required for SERP preview. Example: gsc preview https://mybrand.com/page", Color::RED)
|
|
579
|
+
return
|
|
580
|
+
end
|
|
581
|
+
url ||= "https://#{title.to_s.downcase.gsub(/[^a-z0-9]+/, '-')}.local"
|
|
582
|
+
|
|
583
|
+
sp = GSC::SerpPreview.new(url, title: title, desc: desc)
|
|
422
584
|
data = sp.generate
|
|
423
585
|
|
|
424
586
|
if options[:json]
|
|
@@ -427,29 +589,241 @@ module GSC
|
|
|
427
589
|
end
|
|
428
590
|
|
|
429
591
|
puts BANNER unless options[:in_dashboard]
|
|
592
|
+
m = data[:metrics] || {}
|
|
593
|
+
|
|
430
594
|
puts "š„ļø #{Color::BOLD}GOOGLE SERP PREVIEW (Desktop Viewport):#{Color::RESET}"
|
|
431
|
-
puts "
|
|
432
|
-
puts "ā #{Color.c(data.dig(:desktop_serp, :breadcrumb), Color::DIM)}ā"
|
|
433
|
-
puts "ā #{Color.c(data.dig(:desktop_serp, :title).ljust(
|
|
434
|
-
puts "ā #{Color.c(data.dig(:desktop_serp, :snippet)[0..
|
|
435
|
-
puts "
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
595
|
+
puts "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
596
|
+
puts "ā #{Color.c(data.dig(:desktop_serp, :breadcrumb).to_s.ljust(71), Color::DIM)}ā"
|
|
597
|
+
puts "ā #{Color.c(data.dig(:desktop_serp, :title).to_s.ljust(71), Color::BLUE, Color::BOLD)}ā"
|
|
598
|
+
puts "ā #{Color.c(data.dig(:desktop_serp, :snippet).to_s[0..70].ljust(71), Color::DIM)}ā"
|
|
599
|
+
puts "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
600
|
+
|
|
601
|
+
t_status = if m[:title_truncated]
|
|
602
|
+
Color.c("ā ļø TRUNCATED (+#{(m[:title_pixel_est] - m[:title_desktop_limit]).round(1)}px over 580px limit)", Color::YELLOW, Color::BOLD)
|
|
603
|
+
else
|
|
604
|
+
Color.c('ā
OPTIMAL (< 580px desktop limit)', Color::GREEN, Color::BOLD)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
d_status = if m[:desc_truncated]
|
|
608
|
+
Color.c("ā ļø TRUNCATED (+#{(m[:desc_pixel_est] - m[:desc_desktop_limit]).round(1)}px over 960px limit)", Color::YELLOW, Color::BOLD)
|
|
609
|
+
else
|
|
610
|
+
Color.c('ā
OPTIMAL (< 960px desktop limit)', Color::GREEN, Color::BOLD)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
puts " ⢠Title Width: #{m[:title_chars]} chars / ~#{m[:title_pixel_est]}px [#{t_status}]"
|
|
614
|
+
puts " ⢠Description Width: #{m[:desc_chars]} chars / ~#{m[:desc_pixel_est]}px [#{d_status}]"
|
|
615
|
+
|
|
616
|
+
puts "\nš± #{Color::BOLD}GOOGLE SERP PREVIEW (Mobile Viewport):#{Color::RESET}"
|
|
617
|
+
puts "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
618
|
+
puts "ā #{Color.c(data.dig(:mobile_serp, :breadcrumb).to_s.ljust(71), Color::DIM)}ā"
|
|
619
|
+
puts "ā #{Color.c(data.dig(:mobile_serp, :title).to_s.ljust(71), Color::BLUE, Color::BOLD)}ā"
|
|
620
|
+
puts "ā #{Color.c(data.dig(:mobile_serp, :snippet).to_s[0..70].ljust(71), Color::DIM)}ā"
|
|
621
|
+
puts "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
441
622
|
|
|
442
|
-
puts "\nš± #{Color::BOLD}OPEN GRAPH / SOCIAL CARD PREVIEW:#{Color::RESET}"
|
|
443
623
|
soc = data[:social] || {}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
624
|
+
if soc[:og_title] || soc[:og_image]
|
|
625
|
+
puts "\nš #{Color::BOLD}OPEN GRAPH / SOCIAL CARD PREVIEW:#{Color::RESET}"
|
|
626
|
+
puts " ⢠Title : #{soc[:og_title]}"
|
|
627
|
+
puts " ⢠Description : #{soc[:og_description]}"
|
|
628
|
+
puts " ⢠Card Image : #{soc[:og_image] || '(No og:image specified)'}"
|
|
629
|
+
end
|
|
630
|
+
puts ""
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# 10a. Live SERP Feature Detector & Zero-Click Threat Analysis
|
|
634
|
+
def self.handle_serp_features_command(query, options)
|
|
635
|
+
query_str = query.to_s.sub(/^(query|live):/, '').strip
|
|
636
|
+
query_str = "what is #{Config.default_domain || 'technical seo'}" if query_str.empty?
|
|
637
|
+
|
|
638
|
+
detector = GSC::SerpFeatureDetector.new(query_str, options)
|
|
639
|
+
data = detector.detect
|
|
640
|
+
|
|
641
|
+
if options[:json]
|
|
642
|
+
puts JSON.pretty_generate(data)
|
|
643
|
+
return
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
puts BANNER unless options[:in_dashboard]
|
|
647
|
+
puts "š #{Color::BOLD}LIVE SERP FEATURE & ZERO-CLICK THREAT DETECTOR:#{Color::RESET} #{Color.c(query_str, Color::CYAN, Color::BOLD)}"
|
|
648
|
+
puts "ā" * 80
|
|
649
|
+
|
|
650
|
+
zt = data[:zero_click_threat] || {}
|
|
651
|
+
threat_color = case zt[:level]
|
|
652
|
+
when 'LOW' then Color::GREEN
|
|
653
|
+
when 'MODERATE' then Color::YELLOW
|
|
654
|
+
else Color::RED
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
puts "šÆ Search Intent: #{Color.c(data.dig(:intent, :primary).to_s, Color::BOLD)} (#{data.dig(:intent, :description)})"
|
|
658
|
+
puts "šØ Zero-Click Threat: #{Color.c("#{zt[:level]} (#{zt[:score]}/100)", threat_color, Color::BOLD)} [Organic CTR Impact: #{zt[:estimated_organic_ctr_suppression]}]"
|
|
659
|
+
puts "ā" * 80
|
|
660
|
+
|
|
661
|
+
f = data[:features] || {}
|
|
662
|
+
puts "\n#{Color::BOLD}š DETECTED SERP REAL ESTATE FEATURES:#{Color::RESET}"
|
|
663
|
+
|
|
664
|
+
# AI Overview
|
|
665
|
+
aio = f[:ai_overview] || {}
|
|
666
|
+
aio_badge = aio[:detected] ? Color.c("ACTIVE (#{aio[:probability_pct]}% prob)", Color::RED, Color::BOLD) : Color.c("Unlikely (#{aio[:probability_pct]}%)", Color::GRAY)
|
|
667
|
+
puts " ⢠Google AI Overview (Gemini) : [#{aio_badge}]"
|
|
668
|
+
puts " āā #{Color.c(aio[:impact], Color::DIM)}" if aio[:detected]
|
|
669
|
+
|
|
670
|
+
# Featured Snippet
|
|
671
|
+
fs = f[:featured_snippet] || {}
|
|
672
|
+
fs_badge = fs[:detected] ? Color.c("OPPORTUNITY (#{fs[:target_format].capitalize} - #{fs[:probability_pct]}%)", Color::GREEN, Color::BOLD) : Color.c("None", Color::GRAY)
|
|
673
|
+
puts " ⢠Featured Snippet Box : [#{fs_badge}]"
|
|
674
|
+
puts " āā Target Recipe: #{Color.c(fs[:capture_prescription], Color::CYAN)}" if fs[:detected]
|
|
675
|
+
|
|
676
|
+
# People Also Ask
|
|
677
|
+
paa = f[:people_also_ask] || {}
|
|
678
|
+
paa_badge = paa[:detected] ? Color.c("#{paa[:count]} Live Questions Extracted", Color::CYAN, Color::BOLD) : Color.c("None", Color::GRAY)
|
|
679
|
+
puts " ⢠People Also Ask (PAA) : [#{paa_badge}]"
|
|
680
|
+
if paa[:detected] && (paa[:questions] || []).any?
|
|
681
|
+
paa[:questions].first(5).each do |q_text|
|
|
682
|
+
puts " ā \"#{Color.c(q_text, Color::YELLOW)}\""
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Local Pack
|
|
687
|
+
lp = f[:local_3_pack] || {}
|
|
688
|
+
lp_badge = lp[:detected] ? Color.c("DETECTED (Google Maps 3-Pack)", Color::YELLOW, Color::BOLD) : Color.c("None", Color::GRAY)
|
|
689
|
+
puts " ⢠Local Maps 3-Pack : [#{lp_badge}]"
|
|
690
|
+
|
|
691
|
+
# Video Carousel
|
|
692
|
+
vc = f[:video_carousel] || {}
|
|
693
|
+
vc_badge = vc[:detected] ? Color.c("DETECTED (Video Carousel)", Color::YELLOW, Color::BOLD) : Color.c("None", Color::GRAY)
|
|
694
|
+
puts " ⢠Video Carousel / YouTube : [#{vc_badge}]"
|
|
695
|
+
|
|
696
|
+
# Discussions & Forums
|
|
697
|
+
df = f[:discussions_and_forums] || {}
|
|
698
|
+
df_badge = df[:detected] ? Color.c("DETECTED (Reddit/Quora Modules)", Color::CYAN, Color::BOLD) : Color.c("None", Color::GRAY)
|
|
699
|
+
puts " ⢠Discussions & Forums : [#{df_badge}]"
|
|
700
|
+
|
|
701
|
+
# Shopping Pack
|
|
702
|
+
sp = f[:shopping_pack] || {}
|
|
703
|
+
sp_badge = sp[:detected] ? Color.c("DETECTED (Shopping Product Grid)", Color::YELLOW, Color::BOLD) : Color.c("None", Color::GRAY)
|
|
704
|
+
puts " ⢠Google Shopping Grid : [#{sp_badge}]"
|
|
705
|
+
|
|
706
|
+
# Playbook
|
|
707
|
+
playbook = data[:playbook] || []
|
|
708
|
+
if playbook.any?
|
|
709
|
+
puts "\n#{Color::BOLD}š RICH SNIPPET & CITATION CAPTURE PLAYBOOK:#{Color::RESET}"
|
|
710
|
+
playbook.each_with_index do |p, idx|
|
|
711
|
+
puts " #{idx + 1}. [#{Color.c(p[:priority], Color::BOLD)}] #{Color.c(p[:target], Color::CYAN)}:"
|
|
712
|
+
puts " š #{p[:action]}"
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Live SERP Sampling
|
|
717
|
+
serp = data[:serp_sampling] || []
|
|
718
|
+
if serp.any?
|
|
719
|
+
puts "\n#{Color::BOLD}š LIVE SERP SAMPLING (Top Competitor Results):#{Color::RESET}"
|
|
720
|
+
serp.each do |r|
|
|
721
|
+
puts " #{r[:position]}. #{Color.c(r[:title], Color::BOLD)} [#{Color.c(r[:domain], Color::GRAY)}]"
|
|
722
|
+
puts " āā #{Color.c(r[:url], Color::BLUE)}"
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
puts "\n" + ("ā" * 80) + "\n"
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# 10b. IndexNow Multi-Engine Instant Indexing (Bing, Yandex, Seznam, Naver)
|
|
730
|
+
def self.handle_indexnow_command(target, extra, options)
|
|
731
|
+
if target == 'key' || target == 'connect'
|
|
732
|
+
if extra && !extra.strip.empty?
|
|
733
|
+
key = IndexNow.set_key(extra)
|
|
734
|
+
puts Color.c("ā
Successfully set IndexNow API key to: #{key}", Color::GREEN, Color::BOLD)
|
|
735
|
+
else
|
|
736
|
+
key = IndexNow.get_or_create_key
|
|
737
|
+
host = Config.default_domain || 'yourdomain.com'
|
|
738
|
+
puts "š #{Color::BOLD}INDEXNOW API KEY & HOST SETUP:#{Color::RESET}"
|
|
739
|
+
puts " ⢠Active Key: #{Color.c(key, Color::CYAN, Color::BOLD)}"
|
|
740
|
+
puts " ⢠Host Verification: #{Color.c("https://#{host}/#{key}.txt", Color::BLUE)}"
|
|
741
|
+
puts " ⢠Required Content: #{Color.c(key, Color::BOLD)}"
|
|
742
|
+
puts "\nš” #{Color.c('Verification Tip:', Color::YELLOW)} Create a plain text file at the root of your web server named '#{key}.txt' with '#{key}' as the content."
|
|
743
|
+
end
|
|
744
|
+
return
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
if target == 'sitemap' || target.to_s.end_with?('.xml')
|
|
748
|
+
sitemap_target = (target == 'sitemap') ? extra : target
|
|
749
|
+
handle_indexnow_sitemap_command(sitemap_target, options)
|
|
750
|
+
return
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
target_url = target || options[:url]
|
|
754
|
+
unless target_url
|
|
755
|
+
if Config.default_domain
|
|
756
|
+
target_url = "https://#{Config.default_domain}"
|
|
757
|
+
else
|
|
758
|
+
raise 'Please provide a URL to index (e.g. gsc indexnow https://example.com/new-page) or connect a default domain.'
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
urls = [target_url]
|
|
763
|
+
urls << extra if extra && extra.start_with?('http')
|
|
764
|
+
|
|
765
|
+
key = options[:key] || IndexNow.get_or_create_key
|
|
766
|
+
res = IndexNow.submit(urls, key: key)
|
|
767
|
+
|
|
768
|
+
if options[:json]
|
|
769
|
+
puts JSON.pretty_generate(res)
|
|
770
|
+
return
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
puts BANNER unless options[:in_dashboard]
|
|
774
|
+
puts "ā” #{Color::BOLD}INDEXNOW MULTI-SEARCH ENGINE SUBMISSION:#{Color::RESET}"
|
|
775
|
+
puts "ā" * 75
|
|
776
|
+
status_color = res[:success] ? Color::GREEN : Color::RED
|
|
777
|
+
puts "Status : #{Color.c(res[:message], status_color, Color::BOLD)}"
|
|
778
|
+
puts "Submitted URLs : #{Color.c(res[:submitted_urls].to_s, Color::CYAN, Color::BOLD)}"
|
|
779
|
+
puts "Host : #{res[:host]}"
|
|
780
|
+
puts "Key Location : #{res[:key_location]}"
|
|
781
|
+
puts "Engines Notified: #{Color.c('Microsoft Bing, Yandex, Seznam, Naver', Color::BOLD)}"
|
|
782
|
+
puts "ā" * 75
|
|
783
|
+
|
|
784
|
+
urls.each_with_index do |u, idx|
|
|
785
|
+
puts " #{idx + 1}. #{Color.c(u, Color::CYAN)}"
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
unless res[:success]
|
|
789
|
+
puts "\n#{Color.c('ā ļø Note:', Color::YELLOW)} Ensure you have created #{res[:key_location]} with content: #{res[:key]}"
|
|
790
|
+
end
|
|
791
|
+
puts ""
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def self.handle_indexnow_sitemap_command(target, options)
|
|
795
|
+
sitemap = target || options[:sitemap] || 'sitemap.xml'
|
|
796
|
+
key = options[:key] || IndexNow.get_or_create_key
|
|
797
|
+
limit = options[:limit] ? options[:limit].to_i : nil
|
|
798
|
+
|
|
799
|
+
puts BANNER unless options[:in_dashboard]
|
|
800
|
+
puts "š #{Color::BOLD}BATCH INDEXNOW SITEMAP SUBMISSION:#{Color::RESET} #{Color.c(sitemap, Color::CYAN)}"
|
|
801
|
+
puts "ā" * 75
|
|
802
|
+
|
|
803
|
+
res = IndexNow.submit_sitemap(sitemap, key: key, limit: limit)
|
|
804
|
+
|
|
805
|
+
if options[:json]
|
|
806
|
+
puts JSON.pretty_generate(res)
|
|
807
|
+
return
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
status_color = res[:success] ? Color::GREEN : Color::RED
|
|
811
|
+
puts "Status : #{Color.c(res[:message], status_color, Color::BOLD)}"
|
|
812
|
+
puts "URLs Submitted : #{Color.c(res[:submitted_urls].to_s, Color::GREEN, Color::BOLD)}"
|
|
813
|
+
puts "Host : #{res[:host]}"
|
|
814
|
+
puts "Engines Notified: #{Color.c('Microsoft Bing, Yandex, Seznam, Naver', Color::BOLD)}"
|
|
815
|
+
puts "ā" * 75
|
|
816
|
+
puts "⨠All URLs pushed to IndexNow instant crawl queue."
|
|
447
817
|
puts ""
|
|
448
818
|
end
|
|
449
819
|
|
|
450
820
|
# 11. Network & Redirect Tracer
|
|
451
821
|
def self.handle_trace_command(target, options)
|
|
452
|
-
url = target || Config.default_domain
|
|
822
|
+
url = target || (Config.default_domain ? "https://#{Config.default_domain}" : nil)
|
|
823
|
+
if url.nil? || url.empty?
|
|
824
|
+
puts Color.c("ā Error: Target URL or domain required. Example: gsc trace https://mybrand.com", Color::RED)
|
|
825
|
+
return
|
|
826
|
+
end
|
|
453
827
|
nt = GSC::NetworkTracer.new(url)
|
|
454
828
|
data = nt.trace
|
|
455
829
|
|
|
@@ -476,7 +850,12 @@ module GSC
|
|
|
476
850
|
|
|
477
851
|
# 12. Robots.txt Checker
|
|
478
852
|
def self.handle_robots_command(target, extra, options)
|
|
479
|
-
|
|
853
|
+
raw = target || (Config.default_domain ? "https://#{Config.default_domain}" : nil)
|
|
854
|
+
if raw.nil? || raw.empty?
|
|
855
|
+
puts Color.c("ā Error: Target URL or domain required. Example: gsc robots https://mybrand.com", Color::RED)
|
|
856
|
+
return
|
|
857
|
+
end
|
|
858
|
+
url = raw.to_s.start_with?('http') ? raw : "https://#{raw}"
|
|
480
859
|
path = extra || '/'
|
|
481
860
|
bot = options[:bot] || 'googlebot'
|
|
482
861
|
|
|
@@ -505,7 +884,11 @@ module GSC
|
|
|
505
884
|
|
|
506
885
|
# 13. Backlinks & GSC Links Ingestion
|
|
507
886
|
def self.handle_backlinks_command(target, extra, options)
|
|
508
|
-
domain = target || Config.default_domain
|
|
887
|
+
domain = (target unless target == 'import') || Config.default_domain
|
|
888
|
+
if domain.nil? || domain.empty?
|
|
889
|
+
puts Color.c("ā Error: Target domain required. Example: gsc backlinks mybrand.com", Color::RED)
|
|
890
|
+
return
|
|
891
|
+
end
|
|
509
892
|
bm = GSC::BacklinksManager.new(domain)
|
|
510
893
|
|
|
511
894
|
if target == 'import' || extra == 'import'
|
|
@@ -566,5 +949,565 @@ module GSC
|
|
|
566
949
|
end
|
|
567
950
|
puts ""
|
|
568
951
|
end
|
|
952
|
+
|
|
953
|
+
# 14. Generative Engine Optimization (GEO / AEO) Citability Auditor
|
|
954
|
+
def self.handle_geo_command(target, options)
|
|
955
|
+
raw = target || (Config.default_domain ? "https://#{Config.default_domain}" : nil)
|
|
956
|
+
if raw.nil? || raw.empty?
|
|
957
|
+
puts Color.c("ā Error: Target URL or domain required. Example: gsc geo https://mybrand.com", Color::RED)
|
|
958
|
+
return
|
|
959
|
+
end
|
|
960
|
+
url = raw.to_s.start_with?('http') ? raw : "https://#{raw}"
|
|
961
|
+
auditor = GSC::GeoAuditor.new(url)
|
|
962
|
+
|
|
963
|
+
puts "š¤ Analyzing Generative Engine Optimization (GEO) & Citability for #{Color.c(url, Color::CYAN)}...\n" unless options[:json]
|
|
964
|
+
|
|
965
|
+
data = auditor.audit
|
|
966
|
+
if data[:error]
|
|
967
|
+
if options[:json]
|
|
968
|
+
puts JSON.pretty_generate(data)
|
|
969
|
+
else
|
|
970
|
+
puts Color.c("ā Error: #{data[:message]}", Color::RED, Color::BOLD)
|
|
971
|
+
end
|
|
972
|
+
return
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
if options[:json]
|
|
976
|
+
puts JSON.pretty_generate(data)
|
|
977
|
+
return
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
puts BANNER unless options[:in_dashboard]
|
|
981
|
+
puts "š¤ #{Color::BOLD}GENERATIVE ENGINE OPTIMIZATION (GEO / AEO) AUDIT#{Color::RESET}"
|
|
982
|
+
puts "šÆ Target URL : #{Color.c(data[:url], Color::CYAN)}"
|
|
983
|
+
puts "ā" * 75
|
|
984
|
+
|
|
985
|
+
score_color = case data[:score]
|
|
986
|
+
when 85..100 then Color::GREEN
|
|
987
|
+
when 70..84 then Color::CYAN
|
|
988
|
+
when 50..69 then Color::YELLOW
|
|
989
|
+
else Color::RED
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
puts "š #{Color::BOLD}AI CITABILITY SCORE:#{Color::RESET} #{Color.c("#{data[:score]}/100", score_color, Color::BOLD)} ā #{data[:grade]}"
|
|
993
|
+
puts " ⢠AI Bot Access (Robots.txt) : #{data[:category_scores][:ai_bot_access][:score]}/25"
|
|
994
|
+
puts " ⢠Direct Question Answers : #{data[:category_scores][:direct_answers][:score]}/25"
|
|
995
|
+
puts " ⢠Factual & Data Citability : #{data[:category_scores][:facts_and_data][:score]}/25"
|
|
996
|
+
puts " ⢠Entity Schema & Authority : #{data[:category_scores][:entity_schema][:score]}/25"
|
|
997
|
+
puts "ā" * 75
|
|
998
|
+
|
|
999
|
+
puts "\nš¤ #{Color::BOLD}AI SEARCH CRAWLER ACCESS (Robots.txt):#{Color::RESET}"
|
|
1000
|
+
data[:ai_crawlers][:bots].each do |bot, info|
|
|
1001
|
+
badge = info[:allowed] ? Color.c("ā
ALLOWED", Color::GREEN) : Color.c("ā BLOCKED", Color::RED, Color::BOLD)
|
|
1002
|
+
puts " ⢠#{bot.ljust(20)} [#{info[:provider]}]: #{badge} (#{info[:rule]})"
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
puts "\nš #{Color::BOLD}DIRECT ANSWER & CITABILITY DENSITY:#{Color::RESET}"
|
|
1006
|
+
puts " ⢠Question Headings Detected : #{data[:direct_answers][:total_question_headings]}"
|
|
1007
|
+
puts " ⢠Direct Answer Paragraphs : #{data[:direct_answers][:direct_answer_blocks_found]}"
|
|
1008
|
+
puts " ⢠Optimal Length Answers (30-80w): #{data[:direct_answers][:optimal_answers_count]}"
|
|
1009
|
+
|
|
1010
|
+
samples = data[:direct_answers][:samples] || []
|
|
1011
|
+
if samples.any?
|
|
1012
|
+
puts " #{Color::BOLD}Sample Answer Pairs:#{Color::RESET}"
|
|
1013
|
+
samples.each do |s|
|
|
1014
|
+
puts " Q: #{Color.c(s[:question], Color::CYAN)}"
|
|
1015
|
+
puts " A: #{s[:answer_preview]} (#{s[:word_count]} words)"
|
|
1016
|
+
end
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
puts "\nš #{Color::BOLD}FACTUAL & INFORMATION GAIN SIGNALS:#{Color::RESET}"
|
|
1020
|
+
puts " ⢠Statistics & Percentages : #{data[:facts_and_citability][:percentage_mentions]}"
|
|
1021
|
+
puts " ⢠Currency / Pricing Signals : #{data[:facts_and_citability][:currency_mentions]}"
|
|
1022
|
+
puts " ⢠Structured Bullet Items (<li>): #{data[:facts_and_citability][:bullet_list_items]}"
|
|
1023
|
+
puts " ⢠Comparison Table Rows (<tr>) : #{data[:facts_and_citability][:comparison_table_rows]}"
|
|
1024
|
+
puts " ⢠Recent Year Signals (2024-26): #{data[:facts_and_citability][:recent_year_mentions]}"
|
|
1025
|
+
|
|
1026
|
+
puts "\nšļø #{Color::BOLD}ENTITY KNOWLEDGE GRAPH & SCHEMA:#{Color::RESET}"
|
|
1027
|
+
puts " ⢠Schemas Found : #{data[:entity_knowledge_graph][:schema_count]} (#{data[:entity_knowledge_graph][:schema_types].join(', ')})"
|
|
1028
|
+
puts " ⢠Organization / Brand Schema : #{data[:entity_knowledge_graph][:has_organization_or_brand] ? Color.c("Yes", Color::GREEN) : Color.c("Missing", Color::YELLOW)}"
|
|
1029
|
+
puts " ⢠FAQ / HowTo Rich Schema : #{data[:entity_knowledge_graph][:has_faq_or_howto] ? Color.c("Yes", Color::GREEN) : Color.c("None", Color::YELLOW)}"
|
|
1030
|
+
puts " ⢠Wikidata / Wikipedia Linked : #{data[:entity_knowledge_graph][:wikidata_or_wikipedia_linked] ? Color.c("Yes", Color::GREEN) : Color.c("No", Color::YELLOW)}"
|
|
1031
|
+
if data[:entity_knowledge_graph][:same_as_domains].any?
|
|
1032
|
+
puts " ⢠Social Authority Entity Links: #{data[:entity_knowledge_graph][:same_as_domains].join(', ')}"
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
recs = data[:recommendations] || []
|
|
1036
|
+
if recs.any?
|
|
1037
|
+
puts "\nš” #{Color::BOLD}AI CITABILITY OPTIMIZATION RECOMMENDATIONS:#{Color::RESET}"
|
|
1038
|
+
recs.each_with_index do |r, i|
|
|
1039
|
+
puts " #{i + 1}. #{Color.c(r, Color::YELLOW)}"
|
|
1040
|
+
end
|
|
1041
|
+
end
|
|
1042
|
+
puts ""
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
# 15. Knowledge Graph Entity & sameAs Disambiguation Auditor
|
|
1046
|
+
def self.handle_entity_command(target, options)
|
|
1047
|
+
raw = target || (Config.default_domain ? "https://#{Config.default_domain}" : nil)
|
|
1048
|
+
if raw.nil? || raw.empty?
|
|
1049
|
+
puts Color.c("ā Error: Target URL or domain required. Example: gsc entity https://mybrand.com", Color::RED)
|
|
1050
|
+
return
|
|
1051
|
+
end
|
|
1052
|
+
url = raw.to_s.start_with?('http') ? raw : "https://#{raw}"
|
|
1053
|
+
|
|
1054
|
+
puts "šļø Auditing Knowledge Graph Entity & sameAs Identity for #{Color.c(url, Color::CYAN)}...\n" unless options[:json]
|
|
1055
|
+
|
|
1056
|
+
data = GSC::EntityAuditor.audit(url)
|
|
1057
|
+
if data[:error]
|
|
1058
|
+
if options[:json]
|
|
1059
|
+
puts JSON.pretty_generate(data)
|
|
1060
|
+
else
|
|
1061
|
+
puts Color.c("ā Error: #{data[:error]}", Color::RED, Color::BOLD)
|
|
1062
|
+
end
|
|
1063
|
+
return
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
if options[:json]
|
|
1067
|
+
puts JSON.pretty_generate(data)
|
|
1068
|
+
return
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
puts BANNER unless options[:in_dashboard]
|
|
1072
|
+
puts "šļø #{Color::BOLD}KNOWLEDGE GRAPH ENTITY & sameAs DISAMBIGUATION AUDIT#{Color::RESET}"
|
|
1073
|
+
puts "šÆ Target URL : #{Color.c(data[:url], Color::CYAN)}"
|
|
1074
|
+
puts "ā" * 75
|
|
1075
|
+
|
|
1076
|
+
score_color = case data[:score]
|
|
1077
|
+
when 85..100 then Color::GREEN
|
|
1078
|
+
when 70..84 then Color::CYAN
|
|
1079
|
+
when 50..69 then Color::YELLOW
|
|
1080
|
+
else Color::RED
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
puts "š #{Color::BOLD}ENTITY AUTHORITY SCORE:#{Color::RESET} #{Color.c("#{data[:score]}/100", score_color, Color::BOLD)} ā Grade #{data[:grade]}"
|
|
1084
|
+
puts "š¦ Detected Entities : #{data[:entities_found]}"
|
|
1085
|
+
puts "ā" * 75
|
|
1086
|
+
|
|
1087
|
+
if data[:findings].any?
|
|
1088
|
+
puts "\nš #{Color::BOLD}ENTITY SIGNALS DETECTED:#{Color::RESET}"
|
|
1089
|
+
data[:findings].each do |f|
|
|
1090
|
+
puts " ⢠#{Color.c(f, Color::GREEN)}"
|
|
1091
|
+
end
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
if data[:authoritative_links].any?
|
|
1095
|
+
puts "\nš #{Color::BOLD}HIGH-AUTHORITY SAMEAS IDENTITY LINKS:#{Color::RESET}"
|
|
1096
|
+
data[:authoritative_links].each do |source, link|
|
|
1097
|
+
puts " ⢠#{source.to_s.capitalize.ljust(15)}: #{Color.c(link, Color::CYAN)}"
|
|
1098
|
+
end
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
if data[:recommendations].any?
|
|
1102
|
+
puts "\nš” #{Color::BOLD}OPTIMIZATION DIRECTIVES:#{Color::RESET}"
|
|
1103
|
+
data[:recommendations].each_with_index do |r, i|
|
|
1104
|
+
puts " #{i + 1}. #{Color.c(r, Color::YELLOW)}"
|
|
1105
|
+
end
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
if options[:generate] || data[:score] < 70
|
|
1109
|
+
puts "\nš #{Color::BOLD}RECOMMENDED ENHANCED JSON-LD SCHEMA SNIPPET:#{Color::RESET}"
|
|
1110
|
+
puts "<!-- Paste inside <head> on your homepage/about page -->"
|
|
1111
|
+
puts "<script type=\"application/ld+json\">"
|
|
1112
|
+
puts JSON.pretty_generate(data[:recommended_json_ld])
|
|
1113
|
+
puts "</script>"
|
|
1114
|
+
end
|
|
1115
|
+
puts ""
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
# 16. Multi-Domain Agency Vault & Key Manager
|
|
1119
|
+
def self.handle_vault_command(subcmd, target, extra, options)
|
|
1120
|
+
action = subcmd.to_s.downcase
|
|
1121
|
+
action = 'list' if action.empty? || action == 'vault'
|
|
1122
|
+
|
|
1123
|
+
case action
|
|
1124
|
+
when 'status'
|
|
1125
|
+
stat = GSC::Vault.status
|
|
1126
|
+
if options[:json]
|
|
1127
|
+
puts JSON.pretty_generate(stat)
|
|
1128
|
+
else
|
|
1129
|
+
puts BANNER unless options[:in_dashboard]
|
|
1130
|
+
puts "š #{Color::BOLD}AGENCY CREDENTIAL VAULT SECURITY STATUS#{Color::RESET}"
|
|
1131
|
+
puts "ā" * 70
|
|
1132
|
+
puts " ⢠Vault Directory: #{stat[:vault_dir]}"
|
|
1133
|
+
puts " ⢠Encryption Algorithm: #{Color.c(stat[:encryption_algorithm], Color::GREEN, Color::BOLD)}"
|
|
1134
|
+
puts " ⢠Master Key Present: #{stat[:master_key_present] ? Color.c('Yes (Stored locally)', Color::GREEN) : Color.c('Generated on demand', Color::YELLOW)}"
|
|
1135
|
+
puts " ⢠Stored Client Keys: #{Color.c(stat[:keys_stored].to_s, Color::CYAN, Color::BOLD)}"
|
|
1136
|
+
puts " ⢠Domains Mapped: #{Color.c(stat[:domains_mapped].to_s, Color::CYAN)}"
|
|
1137
|
+
puts " ⢠POSIX 0700 Security: #{stat[:permissions_secure] ? Color.c('ENFORCED', Color::GREEN) : Color.c('PERMISSIVE', Color::YELLOW)}"
|
|
1138
|
+
puts " ⢠Active Target Domain: #{Color.c(stat[:active_domain] || 'None', Color::GREEN, Color::BOLD)}"
|
|
1139
|
+
puts "ā" * 70
|
|
1140
|
+
puts "š” Switch active client anytime: #{Color.c('gsc switch <domain|alias|#>', Color::CYAN)}"
|
|
1141
|
+
puts ""
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
when 'add', 'import'
|
|
1145
|
+
key_path = target
|
|
1146
|
+
unless key_path && File.file?(File.expand_path(key_path))
|
|
1147
|
+
puts Color.c("ā Error: Valid service account JSON path required. Example: gsc vault add ./client-key.json --domain client.com", Color::RED)
|
|
1148
|
+
return
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
domain = options[:domain] || extra
|
|
1152
|
+
alias_name = options[:alias]
|
|
1153
|
+
ga4_id = options[:property]
|
|
1154
|
+
|
|
1155
|
+
begin
|
|
1156
|
+
entry = GSC::Vault.add_key(key_path, domain: domain, alias_name: alias_name, ga4_id: ga4_id)
|
|
1157
|
+
if options[:json]
|
|
1158
|
+
puts JSON.pretty_generate({ status: 'ok', entry: entry })
|
|
1159
|
+
else
|
|
1160
|
+
puts Color.c("ā
Successfully encrypted and added key to Agency Vault!", Color::GREEN, Color::BOLD)
|
|
1161
|
+
puts " ⢠Client Email: #{Color.c(entry['client_email'], Color::CYAN)}"
|
|
1162
|
+
puts " ⢠Domain: #{Color.c(entry['domain'] || 'Not set', Color::GREEN)}"
|
|
1163
|
+
puts " ⢠Alias: #{Color.c(entry['alias'] || 'Not set', Color::YELLOW)}"
|
|
1164
|
+
puts " ⢠Key Vault: #{entry['key_file']} (AES-256-GCM Encrypted)"
|
|
1165
|
+
end
|
|
1166
|
+
rescue StandardError => e
|
|
1167
|
+
puts Color.c("ā Vault Error: #{e.message}", Color::RED)
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
when 'remove', 'rm', 'delete'
|
|
1171
|
+
target_query = target
|
|
1172
|
+
unless target_query
|
|
1173
|
+
puts Color.c("ā Error: Specify domain or alias to remove. Example: gsc vault remove client.com", Color::RED)
|
|
1174
|
+
return
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
ok = GSC::Vault.remove_entry(target_query)
|
|
1178
|
+
if ok
|
|
1179
|
+
puts Color.c("ā
Removed #{target_query} from Agency Vault.", Color::GREEN)
|
|
1180
|
+
else
|
|
1181
|
+
puts Color.c("ā ļø Entry not found in Agency Vault: #{target_query}", Color::YELLOW)
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
when 'list', 'ls'
|
|
1185
|
+
entries = GSC::Vault.list_entries
|
|
1186
|
+
if options[:json]
|
|
1187
|
+
puts JSON.pretty_generate({ vault: GSC::Vault.status, entries: entries })
|
|
1188
|
+
return
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
puts BANNER unless options[:in_dashboard]
|
|
1192
|
+
puts "š #{Color::BOLD}AGENCY CREDENTIAL VAULT (AES-256-GCM Secure Store)#{Color::RESET}"
|
|
1193
|
+
puts "ā" * 75
|
|
1194
|
+
|
|
1195
|
+
if entries.empty?
|
|
1196
|
+
puts Color.c("No client keys registered in Agency Vault yet.", Color::YELLOW)
|
|
1197
|
+
puts "To add a key: #{Color.c('gsc vault add /path/to/key.json --domain client.com --alias client', Color::CYAN)}"
|
|
1198
|
+
puts "ā" * 75
|
|
1199
|
+
return
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
puts "#{Color::BOLD} # | Domain | Alias | Client Account / Email#{Color::RESET}"
|
|
1203
|
+
puts "ā" * 75
|
|
1204
|
+
entries.each_with_index do |e, i|
|
|
1205
|
+
num_str = "[#{i + 1}]".ljust(4)
|
|
1206
|
+
dom_str = (e['domain'] || 'unassigned').ljust(28)
|
|
1207
|
+
alias_str = (e['alias'] || '-').ljust(10)
|
|
1208
|
+
email_str = e['client_email'].to_s
|
|
1209
|
+
marker = e['active'] ? " #{Color.c('š [ACTIVE]', Color::GREEN, Color::BOLD)}" : ""
|
|
1210
|
+
|
|
1211
|
+
puts "#{Color.c(num_str, Color::CYAN)}| #{dom_str} | #{Color.c(alias_str, Color::YELLOW)} | #{email_str}#{marker}"
|
|
1212
|
+
end
|
|
1213
|
+
puts "ā" * 75
|
|
1214
|
+
puts "š” Switch active client anytime: #{Color.c('gsc switch <domain|alias|#>', Color::CYAN)}"
|
|
1215
|
+
puts ""
|
|
1216
|
+
else
|
|
1217
|
+
puts Color.c("Unknown vault action: #{action}. Valid actions: list, add, remove, status", Color::YELLOW)
|
|
1218
|
+
end
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1221
|
+
# 18. Direct Answer & Information Gain Snippet Synthesizer
|
|
1222
|
+
def self.handle_answer_command(query, options = {})
|
|
1223
|
+
if query.nil? || query.to_s.strip.empty?
|
|
1224
|
+
puts Color.c("ā Error: Query required. Usage: gsc answer <query> [--brand <name>] [--type <definition|steps|comparison|faq>] [--generate] [--json]", Color::RED, Color::BOLD)
|
|
1225
|
+
return
|
|
1226
|
+
end
|
|
1227
|
+
|
|
1228
|
+
data = GSC::AnswerSynthesizer.synthesize(query, options)
|
|
1229
|
+
|
|
1230
|
+
if options[:json]
|
|
1231
|
+
puts JSON.pretty_generate(data)
|
|
1232
|
+
return
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
puts BANNER unless options[:in_dashboard]
|
|
1236
|
+
puts "š£ļø #{Color::BOLD}DIRECT ANSWER & INFORMATION GAIN SNIPPET (GEO / AEO)#{Color::RESET}"
|
|
1237
|
+
puts "šÆ Target Query : #{Color.c("\"#{data[:query]}\"", Color::YELLOW, Color::BOLD)}"
|
|
1238
|
+
puts "š·ļø Intent Type : [#{Color.c(data[:intent_type].upcase, Color::MAGENTA, Color::BOLD)}]"
|
|
1239
|
+
puts "š Word Count : #{data[:word_count]} words (#{Color.c("Optimal: #{data[:target_optimal_range]}", Color::GREEN)})"
|
|
1240
|
+
puts "š Citability : #{Color.c("#{data[:citability_score]}/100", Color::GREEN, Color::BOLD)} (Optimized for Featured Snippets, ChatGPT & Perplexity)"
|
|
1241
|
+
puts "ā" * 75
|
|
1242
|
+
|
|
1243
|
+
puts "\n#{Color::BOLD}š¬ OPTIMIZED DIRECT ANSWER PARAGRAPH:#{Color::RESET}"
|
|
1244
|
+
puts " #{Color.c(data[:direct_answer], Color::BOLD)}"
|
|
1245
|
+
|
|
1246
|
+
puts "\n#{Color::BOLD}š STRUCTURED ATTRIBUTES & KEY POINTS:#{Color::RESET}"
|
|
1247
|
+
data[:key_points].each do |kp|
|
|
1248
|
+
clean_kp = kp.gsub(/\*\*(.*?)\*\*/, "#{Color::BOLD}\\1#{Color::RESET}")
|
|
1249
|
+
puts " ⢠#{clean_kp}"
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
puts "\n#{Color::BOLD}š INFORMATION GAIN FACTOID:#{Color::RESET}"
|
|
1253
|
+
puts " š” #{Color.c(data[:information_gain_stat], Color::CYAN)}"
|
|
1254
|
+
|
|
1255
|
+
if options[:generate]
|
|
1256
|
+
puts "\n#{Color::BOLD}š» READY-TO-PASTE HTML SNIPPET:#{Color::RESET}"
|
|
1257
|
+
puts Color.c(data[:html_markup], Color::DIM)
|
|
1258
|
+
|
|
1259
|
+
puts "\n#{Color::BOLD}š SCHEMA.ORG JSON-LD SCRIPT BLOCK:#{Color::RESET}"
|
|
1260
|
+
puts Color.c("<script type=\"application/ld+json\">\n#{JSON.pretty_generate(data[:schema_jsonld])}\n</script>", Color::CYAN)
|
|
1261
|
+
else
|
|
1262
|
+
puts "\nš” #{Color.c('Tip: Pass --generate to print copy-paste HTML and Schema.org JSON-LD blocks.', Color::GRAY)}"
|
|
1263
|
+
end
|
|
1264
|
+
puts "ā" * 75 + "\n"
|
|
1265
|
+
end
|
|
1266
|
+
|
|
1267
|
+
# 19. AI Bot Robots.txt & Cloudflare Firewall Scanner
|
|
1268
|
+
def self.handle_firewall_command(target, options = {})
|
|
1269
|
+
scanner = GSC::FirewallScanner.new(target, options)
|
|
1270
|
+
data = scanner.scan
|
|
1271
|
+
|
|
1272
|
+
if options[:json]
|
|
1273
|
+
puts JSON.pretty_generate(data)
|
|
1274
|
+
return
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
puts BANNER unless options[:in_dashboard]
|
|
1278
|
+
puts "š”ļø #{Color::BOLD}AI BOT ACCESSIBILITY & CLOUDFLARE/WAF FIREWALL AUDITOR#{Color::RESET}"
|
|
1279
|
+
puts "šÆ Target URL : #{Color.c(data[:url], Color::YELLOW, Color::BOLD)}"
|
|
1280
|
+
puts "š Hostname : #{data[:host]}"
|
|
1281
|
+
|
|
1282
|
+
# Edge Infrastructure
|
|
1283
|
+
edge_names = data[:edge_infrastructure].map { |i| "#{i[:name]} (#{i[:role]})" }.join(' | ')
|
|
1284
|
+
puts "āļø Edge CDN/WAF : #{Color.c(edge_names, Color::CYAN)}"
|
|
1285
|
+
|
|
1286
|
+
# Score & Grade
|
|
1287
|
+
grade_color = case data[:grade]
|
|
1288
|
+
when 'A' then Color::GREEN
|
|
1289
|
+
when 'B' then Color::CYAN
|
|
1290
|
+
when 'C' then Color::YELLOW
|
|
1291
|
+
when 'D' then Color::RED
|
|
1292
|
+
else Color::RED
|
|
1293
|
+
end
|
|
1294
|
+
|
|
1295
|
+
score_str = "#{data[:score]}/100 [Grade #{data[:grade]}]"
|
|
1296
|
+
puts "š AI Access : #{Color.c(score_str, Color::BOLD, grade_color)} - #{data[:verdict]}"
|
|
1297
|
+
puts "ā" * 80
|
|
1298
|
+
|
|
1299
|
+
# Section 1: Live HTTP Probes
|
|
1300
|
+
puts "\n#{Color::BOLD}š” LIVE EDGE WAF MULTI-USER-AGENT PROBES (LIVE PASS-THROUGH):#{Color::RESET}"
|
|
1301
|
+
puts "#{Color::BOLD}%-18s %-24s %-12s %-10s %-14s %s#{Color::RESET}" % ['Persona', 'Client Name', 'HTTP Status', 'Latency', 'WAF Verdict', 'Challenge / Block']
|
|
1302
|
+
puts "ā" * 80
|
|
1303
|
+
|
|
1304
|
+
data[:live_probes].each do |probe|
|
|
1305
|
+
status_col = case probe[:status]
|
|
1306
|
+
when 200..299 then Color.c("#{probe[:status]} OK", Color::GREEN)
|
|
1307
|
+
when 300..399 then Color.c("#{probe[:status]} REDIR", Color::YELLOW)
|
|
1308
|
+
when 403 then Color.c("403 FORBID", Color::RED, Color::BOLD)
|
|
1309
|
+
when 503 then Color.c("503 UNAVAIL", Color::RED, Color::BOLD)
|
|
1310
|
+
else Color.c(probe[:status].to_s, Color::RED)
|
|
1311
|
+
end
|
|
1312
|
+
|
|
1313
|
+
verdict_col = case probe[:verdict]
|
|
1314
|
+
when :pass then Color.c("PASS", Color::GREEN)
|
|
1315
|
+
when :redirect then Color.c("REDIRECT", Color::YELLOW)
|
|
1316
|
+
when :challenge then Color.c("CHALLENGE", Color::RED, Color::BOLD)
|
|
1317
|
+
when :blocked then Color.c("BLOCKED", Color::RED, Color::BOLD)
|
|
1318
|
+
else Color.c("ERROR", Color::RED)
|
|
1319
|
+
end
|
|
1320
|
+
|
|
1321
|
+
challenges_str = probe[:challenges].empty? ? '-' : probe[:challenges].join(', ')
|
|
1322
|
+
challenges_col = probe[:challenges].empty? ? Color.c(challenges_str, Color::GRAY) : Color.c(challenges_str, Color::RED, Color::BOLD)
|
|
1323
|
+
|
|
1324
|
+
puts "%-18s %-24s %-21s %-10s %-23s %s" % [
|
|
1325
|
+
probe[:persona],
|
|
1326
|
+
probe[:name],
|
|
1327
|
+
status_col,
|
|
1328
|
+
"#{probe[:duration_ms]}ms",
|
|
1329
|
+
verdict_col,
|
|
1330
|
+
challenges_col
|
|
1331
|
+
]
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
# Section 2: Robots.txt Crawler Governance
|
|
1335
|
+
puts "\n#{Color::BOLD}š¤ ROBOTS.TXT AI SEARCH & TRAINING CRAWLER DIRECTIVES:#{Color::RESET}"
|
|
1336
|
+
puts "#{Color::BOLD}%-20s %-15s %-12s %-12s %s#{Color::RESET}" % ['Bot User-Agent', 'Provider', 'Role', 'Status', 'Matched Rule / Reason']
|
|
1337
|
+
puts "ā" * 80
|
|
1338
|
+
|
|
1339
|
+
data[:robots_txt][:bots].each do |bot_name, bot_info|
|
|
1340
|
+
status_col = case bot_info[:status]
|
|
1341
|
+
when :allowed then Color.c("ALLOWED", Color::GREEN, Color::BOLD)
|
|
1342
|
+
when :restricted then Color.c("RESTRICTED", Color::YELLOW)
|
|
1343
|
+
when :blocked then Color.c("BLOCKED", Color::RED, Color::BOLD)
|
|
1344
|
+
else Color.c("UNKNOWN", Color::GRAY)
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1347
|
+
category_label = bot_info[:category].to_s.sub('_', ' ').capitalize
|
|
1348
|
+
|
|
1349
|
+
puts "%-20s %-15s %-12s %-21s %s" % [
|
|
1350
|
+
bot_name,
|
|
1351
|
+
bot_info[:provider],
|
|
1352
|
+
category_label,
|
|
1353
|
+
status_col,
|
|
1354
|
+
Color.c(bot_info[:reason].to_s, Color::GRAY)
|
|
1355
|
+
]
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1358
|
+
# Section 3: Blockade Diagnosis
|
|
1359
|
+
puts "\n#{Color::BOLD}š SILENT BOT BLOCKADE DIAGNOSIS:#{Color::RESET}"
|
|
1360
|
+
diag = data[:blockade_diagnosis]
|
|
1361
|
+
if diag[:silent_blockade_detected]
|
|
1362
|
+
puts Color.c(" šØ #{diag[:summary]}", Color::RED, Color::BOLD)
|
|
1363
|
+
puts " ā ļø Browser pass: #{diag[:browser_accessible]} | Googlebot pass: #{diag[:googlebot_accessible]}"
|
|
1364
|
+
puts " ā Blocked AI crawlers: #{diag[:blocked_probes].map { |p| "#{p[:name]} (#{p[:status]})" }.join(', ')}"
|
|
1365
|
+
else
|
|
1366
|
+
puts Color.c(" #{diag[:summary]}", Color::GREEN)
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
# Section 4: Prescriptive WAF & Robots.txt Remediation Recipes
|
|
1370
|
+
recipes = data[:remediation_recipes]
|
|
1371
|
+
if recipes[:cloudflare_waf_rule]
|
|
1372
|
+
puts "\n#{Color::BOLD}š ļø RECOMMENDED CLOUDFLARE CUSTOM WAF BYPASS RULE:#{Color::RESET}"
|
|
1373
|
+
puts " Rule Name: #{Color.c(recipes[:cloudflare_waf_rule][:name], Color::CYAN, Color::BOLD)}"
|
|
1374
|
+
puts " Expression:\n #{Color.c(recipes[:cloudflare_waf_rule][:filter_expression], Color::YELLOW)}"
|
|
1375
|
+
puts " Action: #{Color.c('Skip', Color::GREEN, Color::BOLD)} (Skip WAF Managed Rules, Super Bot Fight Mode, Rate Limiting)"
|
|
1376
|
+
puts "\n #{Color::BOLD}Dashboard Deployment Steps:#{Color::RESET}"
|
|
1377
|
+
recipes[:cloudflare_waf_rule][:dashboard_instructions].each do |step|
|
|
1378
|
+
puts " #{Color.c(step, Color::GRAY)}"
|
|
1379
|
+
end
|
|
1380
|
+
end
|
|
1381
|
+
|
|
1382
|
+
if options[:generate] || diag[:silent_blockade_detected] || diag[:robots_critical_blocked].any?
|
|
1383
|
+
puts "\n#{Color::BOLD}š RECOMMENDED ROBOTS.TXT CONFIGURATION FOR AI SEARCH:#{Color::RESET}"
|
|
1384
|
+
puts Color.c(recipes[:recommended_robots_txt], Color::DIM)
|
|
1385
|
+
else
|
|
1386
|
+
puts "\nš” #{Color.c('Tip: Pass --generate to display copy-paste robots.txt configuration.', Color::GRAY)}"
|
|
1387
|
+
end
|
|
1388
|
+
|
|
1389
|
+
puts "ā" * 80 + "\n"
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
# 20. Title Tag Length & Pixel Width Batch Optimizer
|
|
1393
|
+
def self.handle_titles_command(target, options = {})
|
|
1394
|
+
target_site = target.to_s.strip
|
|
1395
|
+
target_site = Config.default_domain || '' if target_site.empty?
|
|
1396
|
+
if target_site.empty?
|
|
1397
|
+
puts Color.c("ā Error: Target domain or sitemap required. Example: gsc titles mybrand.com", Color::RED)
|
|
1398
|
+
return
|
|
1399
|
+
end
|
|
1400
|
+
|
|
1401
|
+
optimizer = GSC::TitleOptimizer.new(target_site, options)
|
|
1402
|
+
|
|
1403
|
+
unless options[:json] || options[:csv]
|
|
1404
|
+
puts BANNER unless options[:in_dashboard]
|
|
1405
|
+
puts "š #{Color::BOLD}GOOGLE SERP TITLE LENGTH & PIXEL OVERFLOW AUDITOR#{Color::RESET}"
|
|
1406
|
+
puts "šÆ Target: #{Color.c(target_site, Color::YELLOW, Color::BOLD)}"
|
|
1407
|
+
print "š Crawling and calculating pixel metrics across pages... "
|
|
1408
|
+
$stdout.flush
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
data = optimizer.audit do |_url, current, total|
|
|
1412
|
+
unless options[:json] || options[:csv]
|
|
1413
|
+
print "\rš Crawling and calculating pixel metrics across pages... [#{current}/#{total}]"
|
|
1414
|
+
$stdout.flush
|
|
1415
|
+
end
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
unless options[:json] || options[:csv]
|
|
1419
|
+
puts " Done!\n"
|
|
1420
|
+
end
|
|
1421
|
+
|
|
1422
|
+
if options[:json]
|
|
1423
|
+
puts JSON.pretty_generate(data)
|
|
1424
|
+
return
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
if options[:csv]
|
|
1428
|
+
require 'csv'
|
|
1429
|
+
puts "URL,Characters,Pixel_Width,Status,Current_Title,Desktop_Preview,Suggested_Rewrite_1,Suggested_Rewrite_2"
|
|
1430
|
+
data[:pages].each do |p|
|
|
1431
|
+
r1 = p[:suggested_rewrites][0] ? p[:suggested_rewrites][0][:title].gsub('"', '""') : ''
|
|
1432
|
+
r2 = p[:suggested_rewrites][1] ? p[:suggested_rewrites][1][:title].gsub('"', '""') : ''
|
|
1433
|
+
title_clean = p[:title].to_s.gsub('"', '""')
|
|
1434
|
+
prev_clean = p[:desktop_preview].to_s.gsub('"', '""')
|
|
1435
|
+
puts "\"#{p[:url]}\",#{p[:char_count]},#{p[:pixel_width]},#{p[:status]},\"#{title_clean}\",\"#{prev_clean}\",\"#{r1}\",\"#{r2}\""
|
|
1436
|
+
end
|
|
1437
|
+
return
|
|
1438
|
+
end
|
|
1439
|
+
|
|
1440
|
+
# Health Grade & Color
|
|
1441
|
+
grade_color = case data[:grade]
|
|
1442
|
+
when 'A' then Color::GREEN
|
|
1443
|
+
when 'B' then Color::CYAN
|
|
1444
|
+
when 'C' then Color::YELLOW
|
|
1445
|
+
when 'D' then Color::RED
|
|
1446
|
+
else Color::RED
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1449
|
+
puts "š Title Health : #{Color.c("#{data[:health_score]}/100 [Grade #{data[:grade]}]", Color::BOLD, grade_color)} - #{data[:verdict]}"
|
|
1450
|
+
puts "š Overview : #{data[:total_pages]} Pages Audited | #{Color.c("#{data[:percentages][:optimal_pct]}% Optimal", Color::GREEN)} | #{Color.c("#{data[:percentages][:overflow_pct]}% Overflowing", Color::YELLOW)}"
|
|
1451
|
+
puts "š Breakdown : ā
#{data[:counts][:optimal]} Optimal | ā ļø #{data[:counts][:desktop_overflow]} Desktop Overflow | šØ #{data[:counts][:critical_overflow]} Critical Overflow | ā¹ļø #{data[:counts][:too_short]} Too Short | ā #{data[:counts][:missing]} Missing"
|
|
1452
|
+
puts "ā" * 80
|
|
1453
|
+
|
|
1454
|
+
pages_to_display = options[:overflow_only] ? data[:pages].select { |p| [:desktop_overflow, :critical_overflow].include?(p[:status]) } : data[:pages]
|
|
1455
|
+
|
|
1456
|
+
if pages_to_display.empty?
|
|
1457
|
+
puts "\nš #{Color.c('All audited pages have optimal title tags with zero pixel overflow!', Color::GREEN, Color::BOLD)}"
|
|
1458
|
+
else
|
|
1459
|
+
puts "\n#{Color::BOLD}%-16s %-7s %-10s %s#{Color::RESET}" % ['Status', 'Chars', 'Width', 'URL & Title Tag']
|
|
1460
|
+
puts "ā" * 80
|
|
1461
|
+
|
|
1462
|
+
pages_to_display.each do |page|
|
|
1463
|
+
status_col = case page[:status]
|
|
1464
|
+
when :optimal then Color.c("OPTIMAL", Color::GREEN)
|
|
1465
|
+
when :desktop_overflow then Color.c("DESK OVERFLOW", Color::YELLOW, Color::BOLD)
|
|
1466
|
+
when :critical_overflow then Color.c("CRIT OVERFLOW", Color::RED, Color::BOLD)
|
|
1467
|
+
when :too_short then Color.c("TOO SHORT", Color::CYAN)
|
|
1468
|
+
when :missing then Color.c("MISSING", Color::RED, Color::BOLD)
|
|
1469
|
+
end
|
|
1470
|
+
|
|
1471
|
+
px_str = "#{page[:pixel_width]}px"
|
|
1472
|
+
px_col = page[:pixel_width] > 580.0 ? Color.c(px_str, Color::RED, Color::BOLD) : Color.c(px_str, Color::GREEN)
|
|
1473
|
+
|
|
1474
|
+
puts "%-23s %-7s %-18s %s" % [
|
|
1475
|
+
status_col,
|
|
1476
|
+
"#{page[:char_count]}c",
|
|
1477
|
+
px_col,
|
|
1478
|
+
Color.c(page[:url], Color::BOLD)
|
|
1479
|
+
]
|
|
1480
|
+
puts " ā³ #{Color.c(page[:title].empty? ? '(No Title Tag)' : page[:title], Color::DIM)}"
|
|
1481
|
+
end
|
|
1482
|
+
end
|
|
1483
|
+
|
|
1484
|
+
# Section: Title Rewrite Optimization Playbook
|
|
1485
|
+
overflowing_pages = data[:pages].reject { |p| p[:suggested_rewrites].empty? }
|
|
1486
|
+
|
|
1487
|
+
if overflowing_pages.any?
|
|
1488
|
+
puts "\n\n#{Color::BOLD}š TITLE REWRITE & CTR OPTIMIZATION PLAYBOOK (#{overflowing_pages.size} Pages Flagged):#{Color::RESET}"
|
|
1489
|
+
puts "ā" * 80
|
|
1490
|
+
|
|
1491
|
+
overflowing_pages.first(8).each_with_index do |p, idx|
|
|
1492
|
+
puts "\n#{Color.c("[#{idx + 1}]", Color::CYAN, Color::BOLD)} #{Color.c(p[:url], Color::BOLD)}"
|
|
1493
|
+
puts " ā Current Title : #{Color.c("\"#{p[:title]}\"", Color::RED)} (#{p[:pixel_width]}px, #{p[:char_count]} chars)"
|
|
1494
|
+
puts " š Desktop SERP : #{Color.c(p[:desktop_preview], Color::GRAY)}"
|
|
1495
|
+
|
|
1496
|
+
puts " š” #{Color::BOLD}Synthesized Non-Truncating Title Rewrites (<560px):#{Color::RESET}"
|
|
1497
|
+
p[:suggested_rewrites].each do |rewrite|
|
|
1498
|
+
puts " ⢠[#{Color.c(rewrite[:type], Color::YELLOW)}] #{Color.c(rewrite[:title], Color::GREEN, Color::BOLD)}"
|
|
1499
|
+
puts " ā³ #{rewrite[:pixel_width]}px | #{rewrite[:char_count]} chars | Desktop SERP: #{Color.c('FITS CLEANLY', Color::GREEN)}"
|
|
1500
|
+
end
|
|
1501
|
+
end
|
|
1502
|
+
|
|
1503
|
+
if overflowing_pages.size > 8
|
|
1504
|
+
puts "\n... and #{overflowing_pages.size - 8} more pages flagged. Run with #{Color.c('--csv', Color::CYAN)} or #{Color.c('--json', Color::CYAN)} to export all rewrites."
|
|
1505
|
+
end
|
|
1506
|
+
end
|
|
1507
|
+
|
|
1508
|
+
puts "\n" + ("ā" * 80) + "\n"
|
|
1509
|
+
end
|
|
569
1510
|
end
|
|
570
1511
|
end
|
|
1512
|
+
|
|
1513
|
+
|