gsc-cli 2.0.1 → 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 +237 -38
- 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 +33 -6
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module GSC
|
|
5
|
+
class PageComparator
|
|
6
|
+
attr_reader :url1, :url2, :data1, :data2
|
|
7
|
+
|
|
8
|
+
def initialize(url1, url2)
|
|
9
|
+
@url1 = url1
|
|
10
|
+
@url2 = url2
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def compare
|
|
14
|
+
pa1 = GSC::PageAnalyzer.new(@url1)
|
|
15
|
+
pa2 = GSC::PageAnalyzer.new(@url2)
|
|
16
|
+
|
|
17
|
+
@data1 = pa1.fetch_and_analyze
|
|
18
|
+
@data2 = pa2.fetch_and_analyze
|
|
19
|
+
|
|
20
|
+
diffs = {
|
|
21
|
+
meta: compare_meta,
|
|
22
|
+
headings: compare_headings,
|
|
23
|
+
images: compare_images,
|
|
24
|
+
links: compare_links,
|
|
25
|
+
performance: compare_perf,
|
|
26
|
+
structured_data: compare_schema
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
page1: { url: @url1, status: @data1[:http_status] },
|
|
31
|
+
page2: { url: @url2, status: @data2[:http_status] },
|
|
32
|
+
comparison: diffs
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def compare_meta
|
|
39
|
+
t1 = @data1.dig(:title, :text) || ''
|
|
40
|
+
t2 = @data2.dig(:title, :text) || ''
|
|
41
|
+
m1 = @data1.dig(:meta_description, :text) || ''
|
|
42
|
+
m2 = @data2.dig(:meta_description, :text) || ''
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
title: {
|
|
46
|
+
page1: { text: t1, length: t1.length, optimal: t1.length.between?(30, 60) },
|
|
47
|
+
page2: { text: t2, length: t2.length, optimal: t2.length.between?(30, 60) }
|
|
48
|
+
},
|
|
49
|
+
meta_description: {
|
|
50
|
+
page1: { text: m1, length: m1.length, optimal: m1.length.between?(70, 155) },
|
|
51
|
+
page2: { text: m2, length: m2.length, optimal: m2.length.between?(70, 155) }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def compare_headings
|
|
57
|
+
h1_1 = @data1.dig(:headings, :h1) || []
|
|
58
|
+
h1_2 = @data2.dig(:headings, :h1) || []
|
|
59
|
+
h2_1 = @data1.dig(:headings, :h2) || []
|
|
60
|
+
h2_2 = @data2.dig(:headings, :h2) || []
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
h1_count: { page1: h1_1.length, page2: h1_2.length },
|
|
64
|
+
h1_text: { page1: h1_1.first, page2: h1_2.first },
|
|
65
|
+
h2_count: { page1: h2_1.length, page2: h2_2.length }
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def compare_images
|
|
70
|
+
img1 = @data1[:images] || {}
|
|
71
|
+
img2 = @data2[:images] || {}
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
total_images: { page1: img1[:total] || 0, page2: img2[:total] || 0 },
|
|
75
|
+
missing_alt: { page1: img1[:missing_alt] || 0, page2: img2[:missing_alt] || 0 }
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def compare_links
|
|
80
|
+
l1 = @data1[:links] || {}
|
|
81
|
+
l2 = @data2[:links] || {}
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
internal: { page1: l1[:internal_count] || 0, page2: l2[:internal_count] || 0 },
|
|
85
|
+
external: { page1: l1[:external_count] || 0, page2: l2[:external_count] || 0 }
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def compare_perf
|
|
90
|
+
{
|
|
91
|
+
response_time_ms: { page1: @data1[:response_time_ms], page2: @data2[:response_time_ms] }
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def compare_schema
|
|
96
|
+
s1 = @data1.dig(:structured_data, :schemas) || []
|
|
97
|
+
s2 = @data2.dig(:structured_data, :schemas) || []
|
|
98
|
+
|
|
99
|
+
types1 = s1.map { |s| s['@type'] }.compact
|
|
100
|
+
types2 = s2.map { |s| s['@type'] }.compact
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
schema_count: { page1: s1.length, page2: s2.length },
|
|
104
|
+
schema_types: { page1: types1, page2: types2 }
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
module GSC
|
|
9
|
+
class PageSpeed
|
|
10
|
+
API_URL = 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed'
|
|
11
|
+
|
|
12
|
+
attr_reader :url, :strategy, :api_key
|
|
13
|
+
|
|
14
|
+
def initialize(url, strategy: 'mobile', api_key: nil)
|
|
15
|
+
@url = url.to_s.strip
|
|
16
|
+
@strategy = strategy.to_s.downcase == 'desktop' ? 'desktop' : 'mobile'
|
|
17
|
+
@api_key = api_key || ENV['PAGESPEED_API_KEY'] || GSC::Config.get('pagespeed_api_key')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
query_params = [
|
|
22
|
+
"url=#{URI.encode_www_form_component(@url)}",
|
|
23
|
+
"strategy=#{@strategy}",
|
|
24
|
+
"category=performance",
|
|
25
|
+
"category=seo"
|
|
26
|
+
]
|
|
27
|
+
query_params << "key=#{URI.encode_www_form_component(@api_key)}" if @api_key && !@api_key.empty?
|
|
28
|
+
|
|
29
|
+
uri = URI("#{API_URL}?#{query_params.join('&')}")
|
|
30
|
+
req = Net::HTTP::Get.new(uri)
|
|
31
|
+
req['User-Agent'] = 'gsc-cli/2.1'
|
|
32
|
+
|
|
33
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30) do |http|
|
|
34
|
+
http.request(req)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if res.code == '200'
|
|
38
|
+
parse_response(JSON.parse(res.body))
|
|
39
|
+
else
|
|
40
|
+
{
|
|
41
|
+
error: true,
|
|
42
|
+
status_code: res.code.to_i,
|
|
43
|
+
message: res.body
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
{ error: true, message: e.message }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def parse_response(data)
|
|
53
|
+
lhr = data['lighthouseResult'] || {}
|
|
54
|
+
categories = lhr['categories'] || {}
|
|
55
|
+
perf_score = ((categories.dig('performance', 'score') || 0) * 100).round
|
|
56
|
+
seo_score = ((categories.dig('seo', 'score') || 0) * 100).round
|
|
57
|
+
|
|
58
|
+
audits = lhr['audits'] || {}
|
|
59
|
+
|
|
60
|
+
# Core Web Vitals
|
|
61
|
+
fcp = audits.dig('first-contentful-paint', 'displayValue')
|
|
62
|
+
lcp = audits.dig('largest-contentful-paint', 'displayValue')
|
|
63
|
+
cls = audits.dig('cumulative-layout-shift', 'displayValue')
|
|
64
|
+
tbt = audits.dig('total-blocking-time', 'displayValue')
|
|
65
|
+
si = audits.dig('speed-index', 'displayValue')
|
|
66
|
+
|
|
67
|
+
# CrUX Field Data (if available)
|
|
68
|
+
crux_metrics = {}
|
|
69
|
+
crux = data.dig('loadingExperience', 'metrics') || {}
|
|
70
|
+
crux.each do |k, v|
|
|
71
|
+
crux_metrics[k] = {
|
|
72
|
+
percentile: v['percentile'],
|
|
73
|
+
category: v['category']
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Opportunities
|
|
78
|
+
opportunities = []
|
|
79
|
+
audits.each do |k, v|
|
|
80
|
+
next unless v['details'] && v['details']['type'] == 'opportunity'
|
|
81
|
+
next unless v['numericValue'] && v['numericValue'] > 100
|
|
82
|
+
|
|
83
|
+
opportunities << {
|
|
84
|
+
id: k,
|
|
85
|
+
title: v['title'],
|
|
86
|
+
savings_ms: v['numericValue'] ? v['numericValue'].round : 0,
|
|
87
|
+
display: v['displayValue']
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
opportunities.sort_by! { |o| -o[:savings_ms] }
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
url: @url,
|
|
94
|
+
strategy: @strategy,
|
|
95
|
+
fetch_time: lhr['fetchTime'],
|
|
96
|
+
performance_score: perf_score,
|
|
97
|
+
seo_score: seo_score,
|
|
98
|
+
metrics: {
|
|
99
|
+
fcp: fcp,
|
|
100
|
+
lcp: lcp,
|
|
101
|
+
cls: cls,
|
|
102
|
+
tbt: tbt,
|
|
103
|
+
speed_index: si
|
|
104
|
+
},
|
|
105
|
+
field_data: crux_metrics,
|
|
106
|
+
opportunities: opportunities.first(5)
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module GSC
|
|
8
|
+
class RobotsChecker
|
|
9
|
+
attr_reader :base_url, :robots_content
|
|
10
|
+
|
|
11
|
+
def initialize(target_url)
|
|
12
|
+
@target_url = target_url.to_s.strip
|
|
13
|
+
@target_url = "https://#{@target_url}" unless @target_url =~ %r{^https?://}
|
|
14
|
+
@uri = URI.parse(@target_url)
|
|
15
|
+
@robots_url = "#{@uri.scheme}://#{@uri.host}:#{@uri.port}/robots.txt"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def fetch_robots_txt
|
|
19
|
+
res = Net::HTTP.get_response(URI.parse(@robots_url))
|
|
20
|
+
return '' unless res.code == '200'
|
|
21
|
+
|
|
22
|
+
res.body.force_encoding('UTF-8')
|
|
23
|
+
rescue StandardError
|
|
24
|
+
''
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def check(path_to_test = nil, user_agent = 'googlebot')
|
|
28
|
+
@robots_content ||= fetch_robots_txt
|
|
29
|
+
path = path_to_test || @uri.path
|
|
30
|
+
path = '/' if path.empty?
|
|
31
|
+
|
|
32
|
+
ua = user_agent.to_s.downcase
|
|
33
|
+
rules = parse_rules(ua)
|
|
34
|
+
|
|
35
|
+
allowed = true
|
|
36
|
+
matched_rule = nil
|
|
37
|
+
|
|
38
|
+
rules.each do |rule|
|
|
39
|
+
pattern = rule[:path]
|
|
40
|
+
regex = Regexp.new('^' + Regexp.escape(pattern).gsub('\*', '.*'))
|
|
41
|
+
if path =~ regex
|
|
42
|
+
allowed = (rule[:type] == :allow)
|
|
43
|
+
matched_rule = rule
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
robots_url: @robots_url,
|
|
49
|
+
user_agent: user_agent,
|
|
50
|
+
tested_path: path,
|
|
51
|
+
allowed: allowed,
|
|
52
|
+
matched_rule: matched_rule,
|
|
53
|
+
has_robots_txt: !@robots_content.empty?
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def parse_rules(target_ua)
|
|
60
|
+
rules = []
|
|
61
|
+
current_ua = nil
|
|
62
|
+
applies = false
|
|
63
|
+
|
|
64
|
+
@robots_content.each_line do |line|
|
|
65
|
+
line = line.strip.sub(/#.*$/, '')
|
|
66
|
+
next if line.empty?
|
|
67
|
+
|
|
68
|
+
if line =~ /^User-agent:\s*(.+)$/i
|
|
69
|
+
current_ua = $1.strip.downcase
|
|
70
|
+
applies = (current_ua == '*' || current_ua == target_ua)
|
|
71
|
+
elsif applies && line =~ /^Disallow:\s*(.*)$/i
|
|
72
|
+
val = $1.strip
|
|
73
|
+
rules << { type: :disallow, path: val } unless val.empty?
|
|
74
|
+
elsif applies && line =~ /^Allow:\s*(.*)$/i
|
|
75
|
+
val = $1.strip
|
|
76
|
+
rules << { type: :allow, path: val } unless val.empty?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
rules
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module GSC
|
|
7
|
+
class SchemaValidator
|
|
8
|
+
attr_reader :url, :schemas, :validation_results
|
|
9
|
+
|
|
10
|
+
def initialize(url)
|
|
11
|
+
@url = url.to_s.strip
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def audit
|
|
15
|
+
pa = GSC::PageAnalyzer.new(@url)
|
|
16
|
+
data = pa.fetch_and_analyze
|
|
17
|
+
@schemas = data.dig(:structured_data, :schemas) || []
|
|
18
|
+
|
|
19
|
+
results = []
|
|
20
|
+
@schemas.each_with_index do |schema, idx|
|
|
21
|
+
results << validate_single_schema(schema, idx)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
url: @url,
|
|
26
|
+
total_schemas: @schemas.length,
|
|
27
|
+
schemas: results
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_single_schema(schema, idx)
|
|
32
|
+
type = schema['@type'] || 'Unknown'
|
|
33
|
+
errors = []
|
|
34
|
+
warnings = []
|
|
35
|
+
|
|
36
|
+
case type
|
|
37
|
+
when 'SoftwareApplication', 'WebApplication'
|
|
38
|
+
errors << 'Missing "name"' unless schema['name']
|
|
39
|
+
warnings << 'Missing "operatingSystem"' unless schema['operatingSystem']
|
|
40
|
+
warnings << 'Missing "applicationCategory"' unless schema['applicationCategory']
|
|
41
|
+
warnings << 'Missing "offers"' unless schema['offers']
|
|
42
|
+
warnings << 'Missing "aggregateRating"' unless schema['aggregateRating']
|
|
43
|
+
|
|
44
|
+
when 'FAQPage'
|
|
45
|
+
main_entity = schema['mainEntity']
|
|
46
|
+
if !main_entity || !main_entity.is_a?(Array) || main_entity.empty?
|
|
47
|
+
errors << 'FAQPage must contain a non-empty "mainEntity" array'
|
|
48
|
+
else
|
|
49
|
+
main_entity.each_with_index do |q, q_idx|
|
|
50
|
+
errors << "Question ##{q_idx + 1} missing name" unless q['name']
|
|
51
|
+
errors << "Question ##{q_idx + 1} missing acceptedAnswer" unless q['acceptedAnswer']
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
when 'Product'
|
|
56
|
+
errors << 'Missing "name"' unless schema['name']
|
|
57
|
+
warnings << 'Missing "image"' unless schema['image']
|
|
58
|
+
warnings << 'Missing "offers"' unless schema['offers']
|
|
59
|
+
|
|
60
|
+
when 'Article', 'BlogPosting'
|
|
61
|
+
errors << 'Missing "headline"' unless schema['headline']
|
|
62
|
+
errors << 'Missing "author"' unless schema['author']
|
|
63
|
+
warnings << 'Missing "datePublished"' unless schema['datePublished']
|
|
64
|
+
warnings << 'Missing "image"' unless schema['image']
|
|
65
|
+
|
|
66
|
+
when 'Organization', 'LocalBusiness'
|
|
67
|
+
errors << 'Missing "name"' unless schema['name']
|
|
68
|
+
errors << 'Missing "url"' unless schema['url']
|
|
69
|
+
warnings << 'Missing "logo"' unless schema['logo']
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
index: idx,
|
|
74
|
+
type: type,
|
|
75
|
+
valid: errors.empty?,
|
|
76
|
+
errors: errors,
|
|
77
|
+
warnings: warnings,
|
|
78
|
+
raw: schema
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.generate_template(type, params = {})
|
|
83
|
+
case type.to_s.downcase
|
|
84
|
+
when 'faq'
|
|
85
|
+
{
|
|
86
|
+
'@context' => 'https://schema.org',
|
|
87
|
+
'@type' => 'FAQPage',
|
|
88
|
+
'mainEntity' => [
|
|
89
|
+
{
|
|
90
|
+
'@type' => 'Question',
|
|
91
|
+
'name' => params[:question] || 'What is PackingLog?',
|
|
92
|
+
'acceptedAnswer' => {
|
|
93
|
+
'@type' => 'Answer',
|
|
94
|
+
'text' => params[:answer] || 'PackingLog is a free moving box and inventory management system.'
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
when 'software', 'app'
|
|
100
|
+
{
|
|
101
|
+
'@context' => 'https://schema.org',
|
|
102
|
+
'@type' => 'SoftwareApplication',
|
|
103
|
+
'name' => params[:name] || 'PackingLog',
|
|
104
|
+
'applicationCategory' => params[:category] || 'UtilitiesApplication',
|
|
105
|
+
'operatingSystem' => 'Web, iOS, Android',
|
|
106
|
+
'offers' => {
|
|
107
|
+
'@type' => 'Offer',
|
|
108
|
+
'price' => '0',
|
|
109
|
+
'priceCurrency' => 'USD'
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else
|
|
113
|
+
{
|
|
114
|
+
'@context' => 'https://schema.org',
|
|
115
|
+
'@type' => 'Organization',
|
|
116
|
+
'name' => params[:name] || 'PackingLog',
|
|
117
|
+
'url' => params[:url] || 'https://packinglog.com'
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module GSC
|
|
5
|
+
class SerpPreview
|
|
6
|
+
attr_reader :url, :data
|
|
7
|
+
|
|
8
|
+
def initialize(url)
|
|
9
|
+
@url = url.to_s.strip
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def generate
|
|
13
|
+
pa = GSC::PageAnalyzer.new(@url)
|
|
14
|
+
@data = pa.fetch_and_analyze
|
|
15
|
+
|
|
16
|
+
title = @data.dig(:title, :text) || 'Untitled Page'
|
|
17
|
+
desc = @data.dig(:meta_description, :text) || 'No meta description found.'
|
|
18
|
+
canonical = @data.dig(:canonical, :url) || @url
|
|
19
|
+
|
|
20
|
+
# SERP pixel calculation approximation:
|
|
21
|
+
# ~10px per character average for Arial 18px title
|
|
22
|
+
title_chars = title.length
|
|
23
|
+
is_truncated = title_chars > 60
|
|
24
|
+
|
|
25
|
+
desktop_title = is_truncated ? "#{title[0..56]}..." : title
|
|
26
|
+
desktop_snippet = desc.length > 155 ? "#{desc[0..152]}..." : desc
|
|
27
|
+
|
|
28
|
+
og = @data[:open_graph] || {}
|
|
29
|
+
twitter = @data[:twitter_card] || {}
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
url: @url,
|
|
33
|
+
canonical: canonical,
|
|
34
|
+
title: title,
|
|
35
|
+
meta_description: desc,
|
|
36
|
+
truncation_risk: is_truncated,
|
|
37
|
+
desktop_serp: {
|
|
38
|
+
title: desktop_title,
|
|
39
|
+
snippet: desktop_snippet,
|
|
40
|
+
breadcrumb: format_breadcrumb(canonical)
|
|
41
|
+
},
|
|
42
|
+
social: {
|
|
43
|
+
og_title: og['og:title'] || title,
|
|
44
|
+
og_description: og['og:description'] || desc,
|
|
45
|
+
og_image: og['og:image'],
|
|
46
|
+
twitter_card: twitter['twitter:card'] || 'summary_large_image'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def format_breadcrumb(url_str)
|
|
54
|
+
uri = URI.parse(url_str) rescue nil
|
|
55
|
+
return url_str unless uri && uri.host
|
|
56
|
+
|
|
57
|
+
domain = uri.host.sub(/^www\./, '')
|
|
58
|
+
parts = uri.path.split('/').reject(&:empty?)
|
|
59
|
+
if parts.empty?
|
|
60
|
+
"https://#{domain}"
|
|
61
|
+
else
|
|
62
|
+
"https://#{domain} > #{parts.join(' > ')}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/gsc/version.rb
CHANGED
data/lib/gsc.rb
CHANGED
|
@@ -26,7 +26,20 @@ require_relative 'gsc/keywords_everywhere'
|
|
|
26
26
|
require_relative 'gsc/prompts'
|
|
27
27
|
require_relative 'gsc/page_analyzer'
|
|
28
28
|
require_relative 'gsc/site_crawler'
|
|
29
|
+
require_relative 'gsc/google_suggest'
|
|
30
|
+
require_relative 'gsc/open_page_rank'
|
|
31
|
+
require_relative 'gsc/page_speed'
|
|
32
|
+
require_relative 'gsc/page_comparator'
|
|
33
|
+
require_relative 'gsc/content_gap'
|
|
34
|
+
require_relative 'gsc/internal_links'
|
|
35
|
+
require_relative 'gsc/schema_validator'
|
|
36
|
+
require_relative 'gsc/llms_generator'
|
|
37
|
+
require_relative 'gsc/serp_preview'
|
|
38
|
+
require_relative 'gsc/network_tracer'
|
|
39
|
+
require_relative 'gsc/robots_checker'
|
|
40
|
+
require_relative 'gsc/backlinks_manager'
|
|
29
41
|
require_relative 'gsc/command_registry'
|
|
42
|
+
require_relative 'gsc/cli_advanced'
|
|
30
43
|
require_relative 'gsc/cli'
|
|
31
44
|
|
|
32
45
|
# Top-level aliases for compatibility
|
|
@@ -36,3 +49,16 @@ KeywordsEverywhere = GSC::KeywordsEverywhere unless defined?(KeywordsEverywhere)
|
|
|
36
49
|
Prompts = GSC::Prompts unless defined?(Prompts)
|
|
37
50
|
PageAnalyzer = GSC::PageAnalyzer unless defined?(PageAnalyzer)
|
|
38
51
|
SiteCrawler = GSC::SiteCrawler unless defined?(SiteCrawler)
|
|
52
|
+
|
|
53
|
+
GoogleSuggest = GSC::GoogleSuggest unless defined?(GoogleSuggest)
|
|
54
|
+
OpenPageRank = GSC::OpenPageRank unless defined?(OpenPageRank)
|
|
55
|
+
PageSpeed = GSC::PageSpeed unless defined?(PageSpeed)
|
|
56
|
+
PageComparator = GSC::PageComparator unless defined?(PageComparator)
|
|
57
|
+
ContentGap = GSC::ContentGap unless defined?(ContentGap)
|
|
58
|
+
InternalLinks = GSC::InternalLinks unless defined?(InternalLinks)
|
|
59
|
+
SchemaValidator = GSC::SchemaValidator unless defined?(SchemaValidator)
|
|
60
|
+
LlmsGenerator = GSC::LlmsGenerator unless defined?(LlmsGenerator)
|
|
61
|
+
SerpPreview = GSC::SerpPreview unless defined?(SerpPreview)
|
|
62
|
+
NetworkTracer = GSC::NetworkTracer unless defined?(NetworkTracer)
|
|
63
|
+
RobotsChecker = GSC::RobotsChecker unless defined?(RobotsChecker)
|
|
64
|
+
BacklinksManager = GSC::BacklinksManager unless defined?(BacklinksManager)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gsc-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ApollosWave LLC
|
|
@@ -9,9 +9,18 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description:
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
description: |
|
|
13
|
+
== OVERVIEW
|
|
14
|
+
|
|
15
|
+
A lightweight, zero-gem CLI tool and AI agent engine for Google Search Console, Google Indexing API, Google Analytics 4, Keywords Everywhere, and Google Trends.
|
|
16
|
+
|
|
17
|
+
== APOLLOSWAVE ECOSYSTEM
|
|
18
|
+
|
|
19
|
+
Built and maintained by {ApollosWave LLC}[https://apolloswave.com/?utm_source=rubygems&utm_medium=gem_desc&utm_campaign=gsc-cli]. Check out our commercial products:
|
|
20
|
+
|
|
21
|
+
* {Superspeed}[https://superspeedapp.com/?utm_source=rubygems&utm_medium=gem_desc&utm_campaign=gsc-cli] - Lightning-fast macOS disk cleaner and RAM booster for Apple Silicon.
|
|
22
|
+
* {Supercart}[https://supercartapp.com/?utm_source=rubygems&utm_medium=gem_desc&utm_campaign=gsc-cli] - High-converting slide cart drawer and 1-click upsells for Shopify stores.
|
|
23
|
+
* {PackingLog}[https://packinglog.com/?utm_source=rubygems&utm_medium=gem_desc&utm_campaign=gsc-cli] - Smart QR-code moving box inventory and photo catalog.
|
|
15
24
|
email:
|
|
16
25
|
- support@apolloswave.com
|
|
17
26
|
executables:
|
|
@@ -19,6 +28,8 @@ executables:
|
|
|
19
28
|
extensions: []
|
|
20
29
|
extra_rdoc_files: []
|
|
21
30
|
files:
|
|
31
|
+
- AUTH.md
|
|
32
|
+
- FUNDING.md
|
|
22
33
|
- LICENSE
|
|
23
34
|
- README.md
|
|
24
35
|
- bin/gsc
|
|
@@ -26,26 +37,42 @@ files:
|
|
|
26
37
|
- lib/gsc.rb
|
|
27
38
|
- lib/gsc/api.rb
|
|
28
39
|
- lib/gsc/auth.rb
|
|
40
|
+
- lib/gsc/backlinks_manager.rb
|
|
29
41
|
- lib/gsc/cli.rb
|
|
42
|
+
- lib/gsc/cli_advanced.rb
|
|
30
43
|
- lib/gsc/client.rb
|
|
31
44
|
- lib/gsc/color.rb
|
|
32
45
|
- lib/gsc/command_registry.rb
|
|
33
46
|
- lib/gsc/config.rb
|
|
47
|
+
- lib/gsc/content_gap.rb
|
|
48
|
+
- lib/gsc/google_suggest.rb
|
|
34
49
|
- lib/gsc/google_trends.rb
|
|
50
|
+
- lib/gsc/internal_links.rb
|
|
35
51
|
- lib/gsc/keyword_planner.rb
|
|
36
52
|
- lib/gsc/keywords_everywhere.rb
|
|
53
|
+
- lib/gsc/llms_generator.rb
|
|
54
|
+
- lib/gsc/network_tracer.rb
|
|
55
|
+
- lib/gsc/open_page_rank.rb
|
|
37
56
|
- lib/gsc/page_analyzer.rb
|
|
57
|
+
- lib/gsc/page_comparator.rb
|
|
58
|
+
- lib/gsc/page_speed.rb
|
|
38
59
|
- lib/gsc/prompts.rb
|
|
60
|
+
- lib/gsc/robots_checker.rb
|
|
61
|
+
- lib/gsc/schema_validator.rb
|
|
62
|
+
- lib/gsc/serp_preview.rb
|
|
39
63
|
- lib/gsc/site_crawler.rb
|
|
40
64
|
- lib/gsc/sitemap_loader.rb
|
|
41
65
|
- lib/gsc/version.rb
|
|
42
|
-
homepage: https://
|
|
66
|
+
homepage: https://apolloswave.com
|
|
43
67
|
licenses:
|
|
44
68
|
- MIT
|
|
45
69
|
metadata:
|
|
46
|
-
homepage_uri: https://
|
|
70
|
+
homepage_uri: https://apolloswave.com/?utm_source=rubygems&utm_medium=gem_sidebar&utm_campaign=gsc-cli
|
|
47
71
|
source_code_uri: https://github.com/ApollosWave/gsc-cli
|
|
72
|
+
documentation_uri: https://github.com/ApollosWave/gsc-cli#readme
|
|
73
|
+
bug_tracker_uri: https://github.com/ApollosWave/gsc-cli/issues
|
|
48
74
|
changelog_uri: https://github.com/ApollosWave/gsc-cli/blob/main/README.md
|
|
75
|
+
funding_uri: https://github.com/ApollosWave/gsc-cli#-sponsorship--backing
|
|
49
76
|
rdoc_options: []
|
|
50
77
|
require_paths:
|
|
51
78
|
- lib
|