gsc-cli 2.0.2 ā 2.1.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 +171 -3
- data/bin/gsc +2028 -197
- data/dist/gsc +167 -5
- data/lib/gsc/backlinks_manager.rb +96 -0
- data/lib/gsc/cli.rb +156 -3
- data/lib/gsc/cli_advanced.rb +570 -0
- data/lib/gsc/config.rb +9 -0
- data/lib/gsc/content_gap.rb +112 -0
- data/lib/gsc/google_suggest.rb +109 -0
- data/lib/gsc/internal_links.rb +132 -0
- data/lib/gsc/llms_generator.rb +104 -0
- data/lib/gsc/network_tracer.rb +86 -0
- data/lib/gsc/open_page_rank.rb +72 -0
- data/lib/gsc/page_comparator.rb +108 -0
- data/lib/gsc/page_speed.rb +110 -0
- data/lib/gsc/robots_checker.rb +83 -0
- data/lib/gsc/schema_validator.rb +122 -0
- data/lib/gsc/serp_preview.rb +66 -0
- data/lib/gsc/version.rb +1 -1
- data/lib/gsc.rb +26 -0
- metadata +17 -2
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module GSC
|
|
8
|
+
class CLI
|
|
9
|
+
# 1. Google Suggest
|
|
10
|
+
def self.handle_suggest_command(target, options)
|
|
11
|
+
query = target.to_s.strip
|
|
12
|
+
if query.empty?
|
|
13
|
+
puts Color.c("ā Error: Query required. Example: gsc suggest \"moving boxes\"", Color::RED)
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
alphabet = options[:alphabet] || false
|
|
18
|
+
suggest = GSC::GoogleSuggest.new(query, options)
|
|
19
|
+
results = suggest.fetch(alphabet: alphabet, questions: false)
|
|
20
|
+
|
|
21
|
+
if options[:json]
|
|
22
|
+
puts JSON.pretty_generate(results)
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
puts BANNER unless options[:in_dashboard]
|
|
27
|
+
puts "š” #{Color::BOLD}GOOGLE SEARCH SUGGESTIONS:#{Color::RESET} #{Color.c(query, Color::CYAN)}"
|
|
28
|
+
puts "ā" * 70
|
|
29
|
+
|
|
30
|
+
if alphabet
|
|
31
|
+
results.each do |key, list|
|
|
32
|
+
next if list.empty?
|
|
33
|
+
prefix = (key == 'root') ? "Root" : "+ #{key.upcase}"
|
|
34
|
+
puts "\n#{Color.c(prefix, Color::BOLD, Color::YELLOW)}:"
|
|
35
|
+
list.each do |item|
|
|
36
|
+
puts " ⢠#{item[:term]}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
if results.empty?
|
|
41
|
+
puts " (No search suggestions returned)"
|
|
42
|
+
else
|
|
43
|
+
results.each_with_index do |item, idx|
|
|
44
|
+
puts " #{Color.c((idx + 1).to_s.rjust(2), Color::DIM)}. #{Color.c(item[:term], Color::BOLD)}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
puts ""
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 2. Questions / PAA
|
|
52
|
+
def self.handle_questions_command(target, options)
|
|
53
|
+
query = target.to_s.strip
|
|
54
|
+
if query.empty?
|
|
55
|
+
puts Color.c("ā Error: Query required. Example: gsc questions \"packing dishes\"", Color::RED)
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
suggest = GSC::GoogleSuggest.new(query, options)
|
|
60
|
+
results = suggest.fetch(questions: true)
|
|
61
|
+
|
|
62
|
+
if options[:json]
|
|
63
|
+
puts JSON.pretty_generate(results)
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
puts BANNER unless options[:in_dashboard]
|
|
68
|
+
puts "ā #{Color::BOLD}SEARCH INTENT QUESTIONS & FAQs:#{Color::RESET} #{Color.c(query, Color::CYAN)}"
|
|
69
|
+
puts "ā" * 70
|
|
70
|
+
|
|
71
|
+
total_found = 0
|
|
72
|
+
results.each do |prefix, list|
|
|
73
|
+
next if list.empty?
|
|
74
|
+
puts "\n#{Color.c(prefix.upcase, Color::BOLD, Color::CYAN)}:"
|
|
75
|
+
list.each do |item|
|
|
76
|
+
total_found += 1
|
|
77
|
+
puts " ⢠#{item[:term]}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if total_found.zero?
|
|
82
|
+
puts " (No question suggestions found for \"#{query}\")"
|
|
83
|
+
end
|
|
84
|
+
puts ""
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 3. OpenPageRank Domain Authority
|
|
88
|
+
def self.handle_authority_command(target, extra, options)
|
|
89
|
+
domains = [target, extra].flatten.compact.reject { |d| d.to_s.strip.empty? }
|
|
90
|
+
domains << Config.default_domain if domains.empty?
|
|
91
|
+
domains = domains.compact
|
|
92
|
+
|
|
93
|
+
if domains.empty?
|
|
94
|
+
puts Color.c("ā Error: Domain required. Example: gsc authority packinglog.com", Color::RED)
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
opr = GSC::OpenPageRank.new
|
|
99
|
+
unless opr.configured?
|
|
100
|
+
puts Color.c("ā ļø OpenPageRank API key not configured.", Color::YELLOW, Color::BOLD)
|
|
101
|
+
puts " Get a 100% free key (300,000 free queries/month) at: #{Color.c('https://openpagerank.com', Color::CYAN)}"
|
|
102
|
+
puts " Then run: #{Color.c('gsc config set opr_api_key <YOUR_KEY>', Color::GREEN)}"
|
|
103
|
+
puts " Or pass: #{Color.c('OPENPAGERANK_API_KEY=<KEY> gsc authority ...', Color::DIM)}"
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
data = opr.check_domains(domains)
|
|
108
|
+
|
|
109
|
+
if options[:json]
|
|
110
|
+
puts JSON.pretty_generate(data)
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
puts BANNER unless options[:in_dashboard]
|
|
115
|
+
puts "š #{Color::BOLD}OPEN PAGERANK & DOMAIN AUTHORITY (Common Crawl Graph):#{Color::RESET}"
|
|
116
|
+
puts "ā" * 75
|
|
117
|
+
|
|
118
|
+
if data[:status] == 'error'
|
|
119
|
+
puts Color.c("ā Error: #{data[:message]}", Color::RED)
|
|
120
|
+
return
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
puts "#{'DOMAIN'.ljust(35)} #{'PAGERANK'.ljust(12)} #{'GLOBAL RANK'.ljust(18)} #{'STATUS'}"
|
|
124
|
+
puts "ā" * 75
|
|
125
|
+
|
|
126
|
+
(data[:records] || []).each do |rec|
|
|
127
|
+
d_name = rec[:domain].to_s.ljust(35)
|
|
128
|
+
pr = sprintf("%.2f / 10", rec[:page_rank_decimal]).ljust(12)
|
|
129
|
+
gr = rec[:rank] ? "##{rec[:rank].to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}".ljust(18) : "N/A".ljust(18)
|
|
130
|
+
st = (rec[:status_code] == 200) ? Color.c("200 OK", Color::GREEN) : Color.c(rec[:status_code].to_s, Color::YELLOW)
|
|
131
|
+
|
|
132
|
+
puts "#{Color.c(d_name, Color::BOLD)} #{Color.c(pr, Color::CYAN)} #{Color.c(gr, Color::YELLOW)} #{st}"
|
|
133
|
+
end
|
|
134
|
+
puts ""
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# 4. PageSpeed Core Web Vitals
|
|
138
|
+
def self.handle_speed_command(target, options)
|
|
139
|
+
url = target || "https://#{Config.default_domain || 'example.com'}"
|
|
140
|
+
url = "https://#{url}" unless url =~ %r{^https?://}
|
|
141
|
+
strategy = options[:strategy] || 'mobile'
|
|
142
|
+
|
|
143
|
+
puts BANNER unless options[:json] || options[:in_dashboard]
|
|
144
|
+
puts "ā” Measuring Core Web Vitals via Google PageSpeed Insights (#{strategy.upcase}):" unless options[:json]
|
|
145
|
+
puts " #{Color.c(url, Color::CYAN)}\n" unless options[:json]
|
|
146
|
+
|
|
147
|
+
ps = GSC::PageSpeed.new(url, strategy: strategy)
|
|
148
|
+
data = ps.run
|
|
149
|
+
|
|
150
|
+
if options[:json]
|
|
151
|
+
puts JSON.pretty_generate(data)
|
|
152
|
+
return
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
if data[:error]
|
|
156
|
+
puts Color.c("ā PageSpeed API Error: #{data[:message]}", Color::RED)
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
perf_score = data[:performance_score]
|
|
161
|
+
score_color = perf_score >= 90 ? Color::GREEN : (perf_score >= 50 ? Color::YELLOW : Color::RED)
|
|
162
|
+
|
|
163
|
+
puts "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
164
|
+
puts "ā Lighthouse Performance Score: #{Color.c(perf_score.to_s.rjust(3) + ' / 100', score_color, Color::BOLD)} ā"
|
|
165
|
+
puts "ā Lighthouse SEO Score : #{Color.c(data[:seo_score].to_s.rjust(3) + ' / 100', Color::GREEN, Color::BOLD)} ā"
|
|
166
|
+
puts "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
167
|
+
|
|
168
|
+
m = data[:metrics] || {}
|
|
169
|
+
puts "\n#{Color::BOLD}š CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
|
|
170
|
+
puts " ⢠LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
171
|
+
puts " ⢠FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
172
|
+
puts " ⢠CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
173
|
+
puts " ⢠TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
174
|
+
puts " ⢠Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
175
|
+
|
|
176
|
+
opps = data[:opportunities] || []
|
|
177
|
+
unless opps.empty?
|
|
178
|
+
puts "\n#{Color::BOLD}š” TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
|
179
|
+
opps.each do |opp|
|
|
180
|
+
puts " ⢠#{opp[:title]}: #{Color.c(opp[:display] || "#{opp[:savings_ms]}ms savings", Color::YELLOW)}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
puts ""
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# 5. Page Comparison
|
|
187
|
+
def self.handle_compare_command(target, extra, options)
|
|
188
|
+
url1 = target
|
|
189
|
+
url2 = extra
|
|
190
|
+
|
|
191
|
+
if url1.nil? || url2.nil?
|
|
192
|
+
puts Color.c("ā Error: Two URLs required. Example: gsc compare https://site.com/p1 https://competitor.com/p2", Color::RED)
|
|
193
|
+
return
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
comp = GSC::PageComparator.new(url1, url2)
|
|
197
|
+
data = comp.compare
|
|
198
|
+
|
|
199
|
+
if options[:json]
|
|
200
|
+
puts JSON.pretty_generate(data)
|
|
201
|
+
return
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
puts BANNER unless options[:in_dashboard]
|
|
205
|
+
puts "š„ #{Color::BOLD}HEAD-TO-HEAD SEO ON-PAGE COMPARISON:#{Color::RESET}"
|
|
206
|
+
puts " Page 1 (Target): #{Color.c(url1, Color::CYAN)}"
|
|
207
|
+
puts " Page 2 (Competitor): #{Color.c(url2, Color::YELLOW)}"
|
|
208
|
+
puts "ā" * 80
|
|
209
|
+
|
|
210
|
+
c = data[:comparison] || {}
|
|
211
|
+
|
|
212
|
+
# Meta Titles
|
|
213
|
+
t1 = c.dig(:meta, :title, :page1) || {}
|
|
214
|
+
t2 = c.dig(:meta, :title, :page2) || {}
|
|
215
|
+
puts "\n#{Color::BOLD}š TITLE TAG:#{Color::RESET}"
|
|
216
|
+
puts " P1: #{t1[:text]} (#{t1[:length]} chars) [#{t1[:optimal] ? Color.c('Optimal', Color::GREEN) : Color.c('Review', Color::YELLOW)}]"
|
|
217
|
+
puts " P2: #{t2[:text]} (#{t2[:length]} chars) [#{t2[:optimal] ? Color.c('Optimal', Color::GREEN) : Color.c('Review', Color::YELLOW)}]"
|
|
218
|
+
|
|
219
|
+
# Headings
|
|
220
|
+
h = c[:headings] || {}
|
|
221
|
+
puts "\n#{Color::BOLD}š·ļø HEADINGS H1 / H2:#{Color::RESET}"
|
|
222
|
+
puts " P1: #{h.dig(:h1_count, :page1)} H1s | #{h.dig(:h2_count, :page1)} H2s"
|
|
223
|
+
puts " P2: #{h.dig(:h1_count, :page2)} H1s | #{h.dig(:h2_count, :page2)} H2s"
|
|
224
|
+
|
|
225
|
+
# Images
|
|
226
|
+
img = c[:images] || {}
|
|
227
|
+
puts "\n#{Color::BOLD}š¼ļø IMAGES & ACCESSIBILITY:#{Color::RESET}"
|
|
228
|
+
puts " P1: #{img.dig(:total_images, :page1)} images (#{img.dig(:missing_alt, :page1)} missing alt)"
|
|
229
|
+
puts " P2: #{img.dig(:total_images, :page2)} images (#{img.dig(:missing_alt, :page2)} missing alt)"
|
|
230
|
+
|
|
231
|
+
# Links
|
|
232
|
+
l = c[:links] || {}
|
|
233
|
+
puts "\n#{Color::BOLD}š LINK COUNTS:#{Color::RESET}"
|
|
234
|
+
puts " P1: #{l.dig(:internal, :page1)} internal | #{l.dig(:external, :page1)} external"
|
|
235
|
+
puts " P2: #{l.dig(:internal, :page2)} internal | #{l.dig(:external, :page2)} external"
|
|
236
|
+
|
|
237
|
+
# Schema
|
|
238
|
+
s = c[:structured_data] || {}
|
|
239
|
+
puts "\n#{Color::BOLD}š¦ STRUCTURED DATA (JSON-LD):#{Color::RESET}"
|
|
240
|
+
puts " P1: #{s.dig(:schema_count, :page1)} schemas #{(s.dig(:schema_types, :page1) || []).inspect}"
|
|
241
|
+
puts " P2: #{s.dig(:schema_count, :page2)} schemas #{(s.dig(:schema_types, :page2) || []).inspect}"
|
|
242
|
+
|
|
243
|
+
# Speed
|
|
244
|
+
p_time = c[:performance] || {}
|
|
245
|
+
puts "\n#{Color::BOLD}ā” RESPONSE TIME:#{Color::RESET}"
|
|
246
|
+
puts " P1: #{p_time.dig(:response_time_ms, :page1)}ms | P2: #{p_time.dig(:response_time_ms, :page2)}ms"
|
|
247
|
+
puts ""
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# 6. Content Gap
|
|
251
|
+
def self.handle_content_gap_command(target, extra, options)
|
|
252
|
+
url1 = target
|
|
253
|
+
url2 = extra
|
|
254
|
+
|
|
255
|
+
if url1.nil? || url2.nil?
|
|
256
|
+
puts Color.c("ā Error: Two URLs required. Example: gsc content-gap https://mysite.com https://competitor.com", Color::RED)
|
|
257
|
+
return
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
gap = GSC::ContentGap.new(url1, url2)
|
|
261
|
+
data = gap.analyze
|
|
262
|
+
|
|
263
|
+
if options[:json]
|
|
264
|
+
puts JSON.pretty_generate(data)
|
|
265
|
+
return
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
puts BANNER unless options[:in_dashboard]
|
|
269
|
+
puts "š #{Color::BOLD}CONTENT & TOPICAL KEYWORD GAP (SurferSEO Style):#{Color::RESET}"
|
|
270
|
+
puts " My URL: #{Color.c(url1, Color::CYAN)} (#{data[:page1][:word_count]} words)"
|
|
271
|
+
puts " Competitor URL: #{Color.c(url2, Color::YELLOW)} (#{data[:page2][:word_count]} words)"
|
|
272
|
+
puts "ā" * 80
|
|
273
|
+
|
|
274
|
+
puts "\n#{Color::BOLD}šÆ HIGH-FREQUENCY PHRASES IN COMPETITOR MISSING IN YOUR CONTENT:#{Color::RESET}"
|
|
275
|
+
unigrams = data[:missing_unigrams] || []
|
|
276
|
+
bigrams = data[:missing_bigrams] || []
|
|
277
|
+
|
|
278
|
+
if unigrams.empty? && bigrams.empty?
|
|
279
|
+
puts " (No major content gap detected! Your page covers competitor terminology well.)"
|
|
280
|
+
else
|
|
281
|
+
puts "\n #{Color.c('Top Missing 2-Word Keyphrases:', Color::BOLD, Color::YELLOW)}"
|
|
282
|
+
bigrams.first(8).each do |b|
|
|
283
|
+
puts " ⢠\"#{Color.c(b[:term], Color::BOLD)}\" (Competitor uses #{b[:competitor_count]}x, You: #{b[:your_count]}x)"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
puts "\n #{Color.c('Top Missing Keywords:', Color::BOLD, Color::CYAN)}"
|
|
287
|
+
unigrams.first(8).each do |u|
|
|
288
|
+
puts " ⢠\"#{Color.c(u[:term], Color::BOLD)}\" (Competitor uses #{u[:competitor_count]}x, You: #{u[:your_count]}x)"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
headings = data[:missing_headings] || []
|
|
293
|
+
unless headings.empty?
|
|
294
|
+
puts "\n#{Color::BOLD}š COMPETITOR HEADINGS / TOPICS YOU OMITTED:#{Color::RESET}"
|
|
295
|
+
headings.each do |h|
|
|
296
|
+
puts " ⢠#{h}"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
puts ""
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# 7. Internal Links Audit
|
|
303
|
+
def self.handle_internal_links_command(target, options)
|
|
304
|
+
base_url = target || "https://#{Config.default_domain || 'example.com'}"
|
|
305
|
+
il = GSC::InternalLinks.new(base_url, limit: options[:limit] || 50)
|
|
306
|
+
|
|
307
|
+
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]
|
|
309
|
+
|
|
310
|
+
data = il.audit
|
|
311
|
+
|
|
312
|
+
if options[:json]
|
|
313
|
+
puts JSON.pretty_generate(data)
|
|
314
|
+
return
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
puts "ā" * 80
|
|
318
|
+
puts "Pages Discovered: #{Color.c(data[:total_pages].to_s, Color::BOLD)}"
|
|
319
|
+
puts "Orphan Pages: #{Color.c(data[:orphans].length.to_s, data[:orphans].empty? ? Color::GREEN : Color::RED, Color::BOLD)}"
|
|
320
|
+
puts "Weakly Linked: #{Color.c(data[:weak_pages].length.to_s, Color::YELLOW, Color::BOLD)} (Only 1 incoming internal link)"
|
|
321
|
+
puts "ā" * 80
|
|
322
|
+
|
|
323
|
+
orphans = data[:orphans] || []
|
|
324
|
+
unless orphans.empty?
|
|
325
|
+
puts "\n#{Color::BOLD}šØ ORPHAN PAGES (0 incoming internal links - Crawl Dead Ends):#{Color::RESET}"
|
|
326
|
+
orphans.each do |orp|
|
|
327
|
+
puts " ⢠#{Color.c(orp, Color::RED)}"
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
puts "\n#{Color::BOLD}š MOST LINKED INTERNAL PAGES:#{Color::RESET}"
|
|
332
|
+
(data[:top_linked] || []).each do |top|
|
|
333
|
+
puts " ⢠#{top[:url]} (#{Color.c(top[:incoming_count].to_s, Color::CYAN)} incoming links)"
|
|
334
|
+
end
|
|
335
|
+
puts ""
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# 8. Schema Validator & Generator
|
|
339
|
+
def self.handle_schema_command(target, extra, options)
|
|
340
|
+
if target == 'generate' || target == 'gen'
|
|
341
|
+
schema_type = extra || 'faq'
|
|
342
|
+
tpl = GSC::SchemaValidator.generate_template(schema_type)
|
|
343
|
+
if options[:json]
|
|
344
|
+
puts JSON.pretty_generate(tpl)
|
|
345
|
+
else
|
|
346
|
+
puts Color.c("š Generated JSON-LD Schema (#{schema_type}):", Color::GREEN, Color::BOLD)
|
|
347
|
+
puts "<script type=\"application/ld+json\">"
|
|
348
|
+
puts JSON.pretty_generate(tpl)
|
|
349
|
+
puts "</script>"
|
|
350
|
+
end
|
|
351
|
+
return
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
url = target || "https://#{Config.default_domain || 'example.com'}"
|
|
355
|
+
sv = GSC::SchemaValidator.new(url)
|
|
356
|
+
data = sv.audit
|
|
357
|
+
|
|
358
|
+
if options[:json]
|
|
359
|
+
puts JSON.pretty_generate(data)
|
|
360
|
+
return
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
puts BANNER unless options[:in_dashboard]
|
|
364
|
+
puts "š¦ #{Color::BOLD}STRUCTURED DATA & RICH SNIPPET VALIDATION:#{Color::RESET} #{Color.c(url, Color::CYAN)}"
|
|
365
|
+
puts "ā" * 75
|
|
366
|
+
|
|
367
|
+
schemas = data[:schemas] || []
|
|
368
|
+
if schemas.empty?
|
|
369
|
+
puts " (No JSON-LD structured data schemas found on this page)"
|
|
370
|
+
puts " Tip: Run `gsc schema generate faq` to create valid JSON-LD schema."
|
|
371
|
+
else
|
|
372
|
+
schemas.each do |sc|
|
|
373
|
+
valid_badge = sc[:valid] ? Color.c("VALID", Color::GREEN, Color::BOLD) : Color.c("INVALID", Color::RED, Color::BOLD)
|
|
374
|
+
puts "\nSchema ##{sc[:index] + 1}: #{Color.c(sc[:type], Color::BOLD)} [#{valid_badge}]"
|
|
375
|
+
(sc[:errors] || []).each { |e| puts " ā Error: #{Color.c(e, Color::RED)}" }
|
|
376
|
+
(sc[:warnings] || []).each { |w| puts " ā ļø Warning: #{Color.c(w, Color::YELLOW)}" }
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
puts ""
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# 9. LLMS.txt & AI Search
|
|
383
|
+
def self.handle_llms_command(target, extra, options)
|
|
384
|
+
base_url = target || "https://#{Config.default_domain || 'example.com'}"
|
|
385
|
+
llms = GSC::LlmsGenerator.new(base_url)
|
|
386
|
+
|
|
387
|
+
if extra == 'audit' || options[:audit]
|
|
388
|
+
data = llms.audit_ai_readability(base_url)
|
|
389
|
+
if options[:json]
|
|
390
|
+
puts JSON.pretty_generate(data)
|
|
391
|
+
else
|
|
392
|
+
puts BANNER unless options[:in_dashboard]
|
|
393
|
+
puts "š¤ #{Color::BOLD}AI SEARCH ENGINE / LLM READABILITY AUDIT:#{Color::RESET} #{Color.c(base_url, Color::CYAN)}"
|
|
394
|
+
puts "ā" * 70
|
|
395
|
+
puts "AI Citation Readiness Score: #{Color.c(data[:ai_readability_score].to_s + '/100', Color::GREEN, Color::BOLD)} [Grade: #{data[:grade]}]"
|
|
396
|
+
puts "\nFeatures Detected:"
|
|
397
|
+
puts " ⢠Single H1 Heading : #{data.dig(:features, :h1_count) == 1 ? 'ā
Yes' : 'ā No'}"
|
|
398
|
+
puts " ⢠Tables for Data : #{data.dig(:features, :has_tables) ? 'ā
Yes' : 'ā No'}"
|
|
399
|
+
puts " ⢠Bullet Lists : #{data.dig(:features, :has_lists) ? 'ā
Yes' : 'ā No'}"
|
|
400
|
+
puts " ⢠Structured Data : #{data.dig(:features, :schemas_found)} schemas"
|
|
401
|
+
unless data[:issues].empty?
|
|
402
|
+
puts "\nOptimizations for Perplexity & ChatGPT:"
|
|
403
|
+
data[:issues].each { |iss| puts " ⢠#{Color.c(iss, Color::YELLOW)}" }
|
|
404
|
+
end
|
|
405
|
+
puts ""
|
|
406
|
+
end
|
|
407
|
+
else
|
|
408
|
+
content = llms.generate_llms_txt
|
|
409
|
+
if options[:save]
|
|
410
|
+
File.write("llms.txt", content)
|
|
411
|
+
puts Color.c("ā
Successfully wrote llms.txt to current directory!", Color::GREEN)
|
|
412
|
+
else
|
|
413
|
+
puts content
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# 10. SERP & Social Preview
|
|
419
|
+
def self.handle_preview_command(target, options)
|
|
420
|
+
url = target || "https://#{Config.default_domain || 'example.com'}"
|
|
421
|
+
sp = GSC::SerpPreview.new(url)
|
|
422
|
+
data = sp.generate
|
|
423
|
+
|
|
424
|
+
if options[:json]
|
|
425
|
+
puts JSON.pretty_generate(data)
|
|
426
|
+
return
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
puts BANNER unless options[:in_dashboard]
|
|
430
|
+
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(59), Color::BLUE, Color::BOLD)}ā"
|
|
434
|
+
puts "ā #{Color.c(data.dig(:desktop_serp, :snippet)[0..58].ljust(59), Color::DIM)}ā"
|
|
435
|
+
puts "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
436
|
+
if data[:truncation_risk]
|
|
437
|
+
puts Color.c("ā ļø Warning: Title exceeds 60 characters and may truncate with '...' on Google SERPs.", Color::YELLOW)
|
|
438
|
+
else
|
|
439
|
+
puts Color.c("ā
Title length is optimal (< 60 chars / ~580px).", Color::GREEN)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
puts "\nš± #{Color::BOLD}OPEN GRAPH / SOCIAL CARD PREVIEW:#{Color::RESET}"
|
|
443
|
+
soc = data[:social] || {}
|
|
444
|
+
puts " ⢠Title : #{soc[:og_title]}"
|
|
445
|
+
puts " ⢠Description : #{soc[:og_description]}"
|
|
446
|
+
puts " ⢠Card Image : #{soc[:og_image] || '(No og:image specified)'}"
|
|
447
|
+
puts ""
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# 11. Network & Redirect Tracer
|
|
451
|
+
def self.handle_trace_command(target, options)
|
|
452
|
+
url = target || Config.default_domain || 'example.com'
|
|
453
|
+
nt = GSC::NetworkTracer.new(url)
|
|
454
|
+
data = nt.trace
|
|
455
|
+
|
|
456
|
+
if options[:json]
|
|
457
|
+
puts JSON.pretty_generate(data)
|
|
458
|
+
return
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
puts BANNER unless options[:in_dashboard]
|
|
462
|
+
puts "š¤ļø #{Color::BOLD}REDIRECT CHAIN & HTTP HEADER TRACE:#{Color::RESET} #{Color.c(url, Color::CYAN)}"
|
|
463
|
+
puts "ā" * 75
|
|
464
|
+
puts "Total Hops: #{data[:total_hops]} | Duration: #{data[:total_duration_ms]}ms"
|
|
465
|
+
|
|
466
|
+
(data[:hops] || []).each do |hop|
|
|
467
|
+
status_c = (hop[:status_code] == 200) ? Color::GREEN : Color::YELLOW
|
|
468
|
+
puts "\nHop ##{hop[:hop]}: #{Color.c(hop[:status_code].to_s, status_c, Color::BOLD)} (#{hop[:duration_ms]}ms)"
|
|
469
|
+
puts " URL: #{hop[:url]}"
|
|
470
|
+
puts " X-Robots-Tag: #{Color.c(hop[:x_robots_tag], Color::RED)}" if hop[:x_robots_tag]
|
|
471
|
+
puts " Canonical: #{hop[:canonical_header]}" if hop[:canonical_header]
|
|
472
|
+
puts " HSTS: #{hop[:hsts] ? 'Enabled' : 'Disabled'}"
|
|
473
|
+
end
|
|
474
|
+
puts ""
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# 12. Robots.txt Checker
|
|
478
|
+
def self.handle_robots_command(target, extra, options)
|
|
479
|
+
url = target || "https://#{Config.default_domain || 'example.com'}"
|
|
480
|
+
path = extra || '/'
|
|
481
|
+
bot = options[:bot] || 'googlebot'
|
|
482
|
+
|
|
483
|
+
rc = GSC::RobotsChecker.new(url)
|
|
484
|
+
data = rc.check(path, bot)
|
|
485
|
+
|
|
486
|
+
if options[:json]
|
|
487
|
+
puts JSON.pretty_generate(data)
|
|
488
|
+
return
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
puts BANNER unless options[:in_dashboard]
|
|
492
|
+
puts "š¤ #{Color::BOLD}ROBOTS.TXT CRAWLER SIMULATOR:#{Color::RESET}"
|
|
493
|
+
puts " Robots URL : #{data[:robots_url]}"
|
|
494
|
+
puts " User Agent : #{Color.c(bot, Color::CYAN)}"
|
|
495
|
+
puts " Test Path : #{Color.c(path, Color::BOLD)}"
|
|
496
|
+
puts "ā" * 70
|
|
497
|
+
|
|
498
|
+
status_badge = data[:allowed] ? Color.c("ā
ALLOWED", Color::GREEN, Color::BOLD) : Color.c("ā BLOCKED (DISALLOW)", Color::RED, Color::BOLD)
|
|
499
|
+
puts "Crawl Verdict : #{status_badge}"
|
|
500
|
+
if data[:matched_rule]
|
|
501
|
+
puts "Matched Rule : #{data[:matched_rule][:type].to_s.upcase}: #{data[:matched_rule][:path]}"
|
|
502
|
+
end
|
|
503
|
+
puts ""
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# 13. Backlinks & GSC Links Ingestion
|
|
507
|
+
def self.handle_backlinks_command(target, extra, options)
|
|
508
|
+
domain = target || Config.default_domain || 'example.com'
|
|
509
|
+
bm = GSC::BacklinksManager.new(domain)
|
|
510
|
+
|
|
511
|
+
if target == 'import' || extra == 'import'
|
|
512
|
+
source = (target == 'import') ? extra : target
|
|
513
|
+
content = if source == 'clip' || source == 'clipboard'
|
|
514
|
+
`pbpaste 2>/dev/null`
|
|
515
|
+
elsif source && File.exist?(source)
|
|
516
|
+
File.read(source)
|
|
517
|
+
else
|
|
518
|
+
nil
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
if content.nil? || content.strip.empty?
|
|
522
|
+
puts Color.c("ā Error: No content provided. Usage: gsc backlinks import [file.csv|clip]", Color::RED)
|
|
523
|
+
return
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
res = bm.import_csv(content)
|
|
527
|
+
if options[:json]
|
|
528
|
+
puts JSON.pretty_generate(res)
|
|
529
|
+
else
|
|
530
|
+
puts Color.c("ā
Successfully imported GSC backlink export!", Color::GREEN, Color::BOLD)
|
|
531
|
+
puts " Referring Domains : #{res[:sources_count]}"
|
|
532
|
+
puts " Target Pages : #{res[:targets_count]}"
|
|
533
|
+
end
|
|
534
|
+
return
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
data = bm.summary
|
|
538
|
+
|
|
539
|
+
if options[:json]
|
|
540
|
+
puts JSON.pretty_generate(data)
|
|
541
|
+
return
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
puts BANNER unless options[:in_dashboard]
|
|
545
|
+
puts "š #{Color::BOLD}GSC BACKLINK & REFERRING DOMAIN INTELLIGENCE:#{Color::RESET} #{Color.c(domain, Color::CYAN)}"
|
|
546
|
+
puts "ā" * 75
|
|
547
|
+
puts "Total Referring Domains : #{Color.c(data[:total_referring_domains].to_s, Color::BOLD)}"
|
|
548
|
+
puts "Total External Links : #{Color.c(data[:total_external_links].to_s, Color::BOLD)}"
|
|
549
|
+
puts "Last Updated : #{data[:updated_at] || 'Never (Run `gsc backlinks import` to ingest GSC export)'}"
|
|
550
|
+
puts "ā" * 75
|
|
551
|
+
|
|
552
|
+
sources = data[:top_referring_domains] || []
|
|
553
|
+
unless sources.empty?
|
|
554
|
+
puts "\n#{Color::BOLD}š TOP REFERRING SITES:#{Color::RESET}"
|
|
555
|
+
sources.each do |s|
|
|
556
|
+
puts " ⢠#{s['domain'].to_s.ljust(45)} #{Color.c(s['links_count'].to_s.rjust(6) + ' links', Color::CYAN)}"
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
targets = data[:top_target_pages] || []
|
|
561
|
+
unless targets.empty?
|
|
562
|
+
puts "\n#{Color::BOLD}šÆ TOP LINKED LANDING PAGES:#{Color::RESET}"
|
|
563
|
+
targets.each do |t|
|
|
564
|
+
puts " ⢠#{t['target_url'].to_s.ljust(45)} #{Color.c(t['incoming_count'].to_s.rjust(6) + ' links', Color::GREEN)}"
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
puts ""
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
end
|
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))
|