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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'base'
|
|
6
|
+
require_relative '../hreflang_validator'
|
|
7
|
+
require_relative '../color'
|
|
8
|
+
|
|
9
|
+
module GSC
|
|
10
|
+
class CLI
|
|
11
|
+
module Hreflang
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def run(command, target, extra, options, api = nil, site_url = nil, hostname = nil)
|
|
15
|
+
target_url = target || (hostname ? "https://#{hostname}" : nil)
|
|
16
|
+
unless target_url
|
|
17
|
+
if File.exist?('public/index.html')
|
|
18
|
+
target_url = 'public/index.html'
|
|
19
|
+
elsif File.exist?('index.html')
|
|
20
|
+
target_url = 'index.html'
|
|
21
|
+
else
|
|
22
|
+
msg = "Please provide a target URL or HTML file: gsc hreflang-check <url|file>"
|
|
23
|
+
if options[:json]
|
|
24
|
+
puts JSON.pretty_generate({ error: msg })
|
|
25
|
+
else
|
|
26
|
+
puts Color.red("❌ Error: #{msg}")
|
|
27
|
+
end
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
puts "🌐 Auditing international hreflang tags & bidirectional reciprocity for #{Color.cyan(target_url)}..." unless options[:json]
|
|
33
|
+
|
|
34
|
+
res = GSC::HreflangValidator.audit(target_url, options)
|
|
35
|
+
|
|
36
|
+
if options[:json]
|
|
37
|
+
puts JSON.pretty_generate(res)
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
render_terminal(res)
|
|
42
|
+
|
|
43
|
+
if options[:csv]
|
|
44
|
+
export_csv(res, options[:csv])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_terminal(res)
|
|
49
|
+
puts "\n#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}"
|
|
50
|
+
puts "#{Color.bold("🌐 INTERNATIONAL HREFLANG RECIPROCITY & ISO CODE AUDITOR")}"
|
|
51
|
+
puts "#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}"
|
|
52
|
+
|
|
53
|
+
score = res[:health_score]
|
|
54
|
+
grade_color = case res[:grade]
|
|
55
|
+
when 'A+', 'A' then Color.green(res[:grade])
|
|
56
|
+
when 'B' then Color.cyan(res[:grade])
|
|
57
|
+
when 'C' then Color.yellow(res[:grade])
|
|
58
|
+
else Color.red(res[:grade])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
puts " • Hreflang Health Score: [ #{Color.bold(grade_color)} ] #{Color.bold("#{score}/100")}"
|
|
62
|
+
puts " • Total Alternate Links Found: #{Color.bold(res[:total_tags].to_s)} declarations"
|
|
63
|
+
puts " • Target URL / Source: #{res[:url]}"
|
|
64
|
+
puts "#{Color.bold("───────────────────────────────────────────────────────────────────────────")}"
|
|
65
|
+
|
|
66
|
+
self_badge = res[:self_reference] ? Color.green("✅ Present") : Color.red("❌ Missing (Self-reference is required)")
|
|
67
|
+
x_badge = res[:has_x_default] ? Color.green("✅ Present") : Color.yellow("⚠️ Missing (Recommended fallback)")
|
|
68
|
+
|
|
69
|
+
puts " Core International Specifications:"
|
|
70
|
+
puts " 🔁 Self-Referencing Tag: #{self_badge}"
|
|
71
|
+
puts " 🌍 x-default Global Fallback: #{x_badge}"
|
|
72
|
+
puts " ⇄ Confirmed Reciprocal Links: #{Color.bold(res[:reciprocal_count].to_s)}"
|
|
73
|
+
puts " ⇥ Unreciprocated Return Links: #{res[:unreciprocated_count] > 0 ? Color.red(res[:unreciprocated_count].to_s) : Color.green("0")}"
|
|
74
|
+
puts " ⚠️ Invalid Language/Region Codes: #{res[:invalid_code_count] > 0 ? Color.red(res[:invalid_code_count].to_s) : Color.green("0")}"
|
|
75
|
+
puts "#{Color.bold("───────────────────────────────────────────────────────────────────────────")}\n"
|
|
76
|
+
|
|
77
|
+
if res[:tags].empty?
|
|
78
|
+
puts Color.yellow("⚠️ No hreflang alternate tags discovered in target document or response headers.")
|
|
79
|
+
puts " Add <link rel=\"alternate\" hreflang=\"...\" href=\"...\" /> tags to designate localized variants.\n"
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
puts "#{Color.bold("HREFLANG CLUSTER DECLARATIONS:")}\n\n"
|
|
84
|
+
printf " %-12s %-40s %-16s %s\n", "Hreflang", "Destination URL", "Status", "Issues / Notes"
|
|
85
|
+
puts " ---------------------------------------------------------------------------------------"
|
|
86
|
+
|
|
87
|
+
res[:tags].each do |tag|
|
|
88
|
+
dest = tag[:href].length > 38 ? "#{tag[:href][0..35]}..." : tag[:href]
|
|
89
|
+
|
|
90
|
+
status_str = case tag[:status]
|
|
91
|
+
when :self_referential then Color.cyan("🔁 Self-Ref")
|
|
92
|
+
when :confirmed then Color.green("⇄ Confirmed")
|
|
93
|
+
when :missing_return then Color.red("⇥ No Return")
|
|
94
|
+
when :http_error then Color.red("❌ HTTP Error")
|
|
95
|
+
when :unreachable then Color.gray("⚠️ Unreachable")
|
|
96
|
+
else Color.gray("— Pending")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
issue_notes = if tag[:correction]
|
|
100
|
+
Color.yellow("Fix: #{tag[:correction]}")
|
|
101
|
+
elsif tag[:issues].empty?
|
|
102
|
+
Color.green("Valid Code")
|
|
103
|
+
else
|
|
104
|
+
tag[:issues].map { |i| format_issue(i) }.join(', ')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
printf " %-12s %-40s %-16s %s\n", tag[:hreflang], dest, status_str, issue_notes
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
puts "\n#{Color.bold("───────────────────────────────────────────────────────────────────────────")}"
|
|
111
|
+
|
|
112
|
+
if res[:prescriptions].any?
|
|
113
|
+
puts "#{Color.bold("PRIORITIZED INTERNATIONAL SEO PRESCRIPTIONS:")}"
|
|
114
|
+
res[:prescriptions].each_with_index do |p, idx|
|
|
115
|
+
puts " #{idx + 1}. 💡 #{p}"
|
|
116
|
+
end
|
|
117
|
+
puts
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if res[:cluster_snippets][:html] && !res[:cluster_snippets][:html].empty?
|
|
121
|
+
puts "#{Color.bold("1-CLICK STANDARDIZED HTML CLUSTER CODE:")}"
|
|
122
|
+
puts "```html"
|
|
123
|
+
puts res[:cluster_snippets][:html]
|
|
124
|
+
puts "```\n"
|
|
125
|
+
|
|
126
|
+
puts "#{Color.bold("1-CLICK GOOGLE XML SITEMAP <XHTML:LINK> CODE:")}"
|
|
127
|
+
puts "```xml"
|
|
128
|
+
puts res[:cluster_snippets][:sitemap_xml]
|
|
129
|
+
puts "```\n"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
puts "#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}\n"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def format_issue(issue)
|
|
136
|
+
case issue
|
|
137
|
+
when :invalid_language then Color.red("Invalid Language")
|
|
138
|
+
when :invalid_region then Color.red("Invalid Region")
|
|
139
|
+
when :common_mistake then Color.yellow("Common Code Error")
|
|
140
|
+
when :uppercase_language then Color.yellow("Uppercase Lang (Use Lowercase)")
|
|
141
|
+
when :missing_reciprocal_tag then Color.red("Missing Return Tag")
|
|
142
|
+
when :invalid_script then Color.red("Invalid Script")
|
|
143
|
+
when :lowercase_region then Color.yellow("Lowercase Region (Use Uppercase)")
|
|
144
|
+
else issue.to_s
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def export_csv(res, file_path)
|
|
149
|
+
headers = %w[Hreflang Destination Status Source Issues]
|
|
150
|
+
rows = res[:tags].map do |t|
|
|
151
|
+
[
|
|
152
|
+
t[:hreflang],
|
|
153
|
+
t[:href],
|
|
154
|
+
t[:status].to_s,
|
|
155
|
+
t[:source].to_s,
|
|
156
|
+
t[:issues].join('|')
|
|
157
|
+
]
|
|
158
|
+
end
|
|
159
|
+
Base.write_csv(file_path, headers, rows)
|
|
160
|
+
puts Color.cyan("📁 Successfully exported hreflang audit to #{file_path}")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'base'
|
|
6
|
+
require_relative '../image_seo'
|
|
7
|
+
require_relative '../color'
|
|
8
|
+
|
|
9
|
+
module GSC
|
|
10
|
+
class CLI
|
|
11
|
+
module ImageSeo
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def run(command, target, extra, options, api = nil, site_url = nil, hostname = nil)
|
|
15
|
+
target_url = target || (hostname ? "https://#{hostname}" : nil)
|
|
16
|
+
unless target_url
|
|
17
|
+
if File.exist?('public/index.html')
|
|
18
|
+
target_url = 'public/index.html'
|
|
19
|
+
elsif File.exist?('index.html')
|
|
20
|
+
target_url = 'index.html'
|
|
21
|
+
else
|
|
22
|
+
msg = "Please provide a target URL or HTML file: gsc image-seo <url|file>"
|
|
23
|
+
if options[:json]
|
|
24
|
+
puts JSON.pretty_generate({ error: msg })
|
|
25
|
+
else
|
|
26
|
+
puts Color.red("❌ Error: #{msg}")
|
|
27
|
+
end
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
puts "🖼️ Auditing image SEO, dimensions & next-gen formats for #{Color.cyan(target_url)}..." unless options[:json]
|
|
33
|
+
|
|
34
|
+
res = GSC::ImageSeo.audit(target_url, options)
|
|
35
|
+
|
|
36
|
+
if options[:json]
|
|
37
|
+
puts JSON.pretty_generate(res)
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
render_terminal(res)
|
|
42
|
+
|
|
43
|
+
if options[:csv]
|
|
44
|
+
export_csv(res, options[:csv])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_terminal(res)
|
|
49
|
+
puts "\n#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}"
|
|
50
|
+
puts "#{Color.bold("🖼️ IMAGE SEO & NEXT-GEN FORMAT AUDITOR (WebP • AVIF • CLS • Alt)")}"
|
|
51
|
+
puts "#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}"
|
|
52
|
+
|
|
53
|
+
score = res[:health_score]
|
|
54
|
+
grade_color = case res[:grade]
|
|
55
|
+
when 'A+', 'A' then Color.green(res[:grade])
|
|
56
|
+
when 'B' then Color.cyan(res[:grade])
|
|
57
|
+
when 'C' then Color.yellow(res[:grade])
|
|
58
|
+
else Color.red(res[:grade])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
puts " • Image SEO Health Score: [ #{Color.bold(grade_color)} ] #{Color.bold("#{score}/100")}"
|
|
62
|
+
puts " • Total Images Discovered: #{Color.bold(res[:total_images].to_s)} images"
|
|
63
|
+
puts " • Target URL / Source: #{res[:url]}"
|
|
64
|
+
puts "#{Color.bold("───────────────────────────────────────────────────────────────────────────")}"
|
|
65
|
+
|
|
66
|
+
m = res[:metrics]
|
|
67
|
+
puts " Image Compliance Dimensions:"
|
|
68
|
+
puts " 🏷️ Alt Attribute Coverage: #{Color.bold("#{m[:alt_coverage_pct]}%")}"
|
|
69
|
+
puts " 📐 Explicit Width & Height (CLS): #{Color.bold("#{m[:dimension_coverage_pct]}%")}"
|
|
70
|
+
puts " ⚡ Next-Gen Formats (WebP/AVIF): #{Color.bold("#{m[:modern_format_pct]}%")}"
|
|
71
|
+
puts " ⏳ Proper Lazy-Loading / LCP: #{Color.bold("#{m[:lazy_loading_pct]}%")}"
|
|
72
|
+
puts "#{Color.bold("───────────────────────────────────────────────────────────────────────────")}\n"
|
|
73
|
+
|
|
74
|
+
if res[:images].empty?
|
|
75
|
+
puts Color.green("🎉 No image tags found on target page.")
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
puts "#{Color.bold("IMAGE ASSET AUDIT BREAKDOWN (First #{res[:images].size} of #{res[:total_images]} images):")}\n\n"
|
|
80
|
+
printf " %-4s %-32s %-8s %-12s %-10s %s\n", "#", "Source File", "Format", "Dimensions", "Alt Status", "Issues"
|
|
81
|
+
puts " ---------------------------------------------------------------------------------------"
|
|
82
|
+
|
|
83
|
+
res[:images].each do |img|
|
|
84
|
+
filename = File.basename(img[:src].split('?').first)
|
|
85
|
+
short_file = filename.length > 30 ? "#{filename[0..27]}..." : filename
|
|
86
|
+
|
|
87
|
+
dim_str = (img[:width] && img[:height]) ? "#{img[:width]}x#{img[:height]}" : Color.red("MISSING")
|
|
88
|
+
|
|
89
|
+
alt_status = if img[:alt].nil?
|
|
90
|
+
Color.red("MISSING")
|
|
91
|
+
elsif img[:alt].empty?
|
|
92
|
+
img[:is_decorative] ? Color.gray("DECORATIVE") : Color.yellow("EMPTY")
|
|
93
|
+
else
|
|
94
|
+
Color.green("OK")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
issues_summary = if img[:issues].empty?
|
|
98
|
+
Color.green("✅ Clean")
|
|
99
|
+
else
|
|
100
|
+
img[:issues].map { |i| format_issue(i) }.join(', ')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
printf " %-4s %-32s %-8s %-12s %-10s %s\n", img[:index], short_file, img[:format].upcase, dim_str, alt_status, issues_summary
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
puts "\n#{Color.bold("───────────────────────────────────────────────────────────────────────────")}"
|
|
107
|
+
|
|
108
|
+
if res[:prescriptions].any?
|
|
109
|
+
puts "#{Color.bold("PRIORITIZED IMAGE SEO PRESCRIPTIONS:")}"
|
|
110
|
+
res[:prescriptions].each_with_index do |p, idx|
|
|
111
|
+
puts " #{idx + 1}. 💡 #{p}"
|
|
112
|
+
end
|
|
113
|
+
puts
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Code Snippet Preview for the first flagged image
|
|
117
|
+
sample_img = res[:images].find { |i| i[:issues].any? } || res[:images].first
|
|
118
|
+
if sample_img
|
|
119
|
+
puts "#{Color.bold("1-CLICK MODERN <PICTURE> CODE FIX (For #{File.basename(sample_img[:src])}):")}"
|
|
120
|
+
puts "```html"
|
|
121
|
+
puts sample_img[:picture_tag_snippet]
|
|
122
|
+
puts "```\n"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
puts "#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}\n"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def format_issue(issue)
|
|
129
|
+
case issue
|
|
130
|
+
when :missing_alt then Color.red("Missing Alt")
|
|
131
|
+
when :empty_alt then Color.yellow("Empty Alt")
|
|
132
|
+
when :missing_dimensions then Color.red("No Width/Height (CLS)")
|
|
133
|
+
when :legacy_format then Color.yellow("Legacy Format")
|
|
134
|
+
when :lcp_lazy_loaded then Color.red("Hero Lazy Loaded (LCP Risk)")
|
|
135
|
+
when :hero_missing_priority then Color.yellow("Hero Needs Priority")
|
|
136
|
+
when :missing_lazy then Color.gray("Needs Lazy")
|
|
137
|
+
when :oversized_payload then Color.red("Oversized >200KB")
|
|
138
|
+
else issue.to_s
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def export_csv(res, file_path)
|
|
143
|
+
headers = %w[Index Source Format Width Height Alt Loading Hero Issues]
|
|
144
|
+
rows = res[:images].map do |img|
|
|
145
|
+
[
|
|
146
|
+
img[:index],
|
|
147
|
+
img[:src],
|
|
148
|
+
img[:format],
|
|
149
|
+
img[:width] || '',
|
|
150
|
+
img[:height] || '',
|
|
151
|
+
img[:alt] || '',
|
|
152
|
+
img[:loading] || '',
|
|
153
|
+
img[:is_hero] ? 'YES' : 'NO',
|
|
154
|
+
img[:issues].join('|')
|
|
155
|
+
]
|
|
156
|
+
end
|
|
157
|
+
Base.write_csv(file_path, headers, rows)
|
|
158
|
+
puts Color.cyan("📁 Successfully exported image audit to #{file_path}")
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|