gsc-cli 2.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +500 -0
- data/bin/gsc +8128 -0
- data/dist/gsc +8128 -0
- data/lib/gsc/api.rb +300 -0
- data/lib/gsc/auth.rb +92 -0
- data/lib/gsc/cli.rb +5435 -0
- data/lib/gsc/client.rb +79 -0
- data/lib/gsc/color.rb +22 -0
- data/lib/gsc/command_registry.rb +335 -0
- data/lib/gsc/config.rb +178 -0
- data/lib/gsc/google_trends.rb +187 -0
- data/lib/gsc/keyword_planner.rb +256 -0
- data/lib/gsc/keywords_everywhere.rb +144 -0
- data/lib/gsc/page_analyzer.rb +431 -0
- data/lib/gsc/prompts.rb +285 -0
- data/lib/gsc/site_crawler.rb +260 -0
- data/lib/gsc/sitemap_loader.rb +91 -0
- data/lib/gsc/version.rb +5 -0
- data/lib/gsc.rb +38 -0
- metadata +67 -0
data/lib/gsc/prompts.rb
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GSC
|
|
4
|
+
class Prompts
|
|
5
|
+
PLAYBOOKS = [
|
|
6
|
+
# Master 360° Multi-Horizon Audit
|
|
7
|
+
{
|
|
8
|
+
id: 0,
|
|
9
|
+
category: 'master',
|
|
10
|
+
category_name: '👑 Master 360° Multi-Horizon Audits',
|
|
11
|
+
title: 'The 360° Multi-Horizon Master SEO & Universal Keyword Audit',
|
|
12
|
+
impact: 'Maximum Impact (Exhaustive 30d, 90d, 180d audit & action plan)',
|
|
13
|
+
cli_command: 'gsc performance --days 180 --json',
|
|
14
|
+
template: "Antigravity, perform an exhaustive 360° SEO, Universal Keyword, and Crawl Health Audit for {{domain}} across 30d, 90d, and 180d horizons.\n\nExecute the following commands with --json and save the final report to both an Antigravity Artifact and to docs/seo/master_audit_report.md:\n\n1. Macro Multi-Horizon Performance & Trends:\n - `gsc performance --days 30 --json`\n - `gsc performance --days 90 --json`\n - `gsc performance --days 180 --json`\n - `gsc decay --compare 28 --json`\n\n2. Universal Keyword Intelligence (Saved & Unsaved):\n - Check all saved snapshots: `gsc saved check 1 --json`\n - Unsaved live search queries: `gsc top-queries --days 180 --limit 500 -s imp --json`\n - Page 2 striking distance: `gsc opportunities --min-imp 5 --limit 100 --json`\n - CTR underperformers: `gsc underperformers --limit 50 --json`\n - Keyword cannibalization conflicts: `gsc cannibalization --json`\n\n3. Google Trends & Autocomplete Velocity:\n - `gsc trends \"{{seed}}\" --time 12m --json`\n - `gsc planner \"{{seed}}\" --json`\n\n4. Landing Pages, Post-Click GA4 Behavior & Crawl Budget:\n - Top traffic landing pages: `gsc top-pages --days 90 --limit 100 --json`\n - Organic bounce & engagement: `gsc ga4 --organic --days 30 --limit 100 --json`\n - SERP vs Bounce correlation: `gsc correlation --organic --json`\n - Sitemap crawl waste: `gsc zombies public/sitemap.xml --json`\n\nGenerate a Master Executive Report containing:\n1. Executive Growth Scorecard (Macro comparison: 30d vs 90d vs 180d clicks, impressions, CTR, pos).\n2. The Universal Keyword Opportunity Matrix (Ranking, Striking Distance, Untargeted Saved Keywords).\n3. Google Trends Seasonal Velocity & Rising Breakouts (+5000% queries).\n4. CTR Optimization Matrix (Title & meta rewrites for low-CTR pages).\n5. Post-Click Leak Audit (High organic impressions with high GA4 bounce).\n6. Technical & Zombie Crawl Waste cleanup plan.\n7. Prioritized 30-Day Sprint (P0, P1, P2 with exact file paths and code edits)."
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
# Category: Growth & Striking Distance Opportunities
|
|
18
|
+
{
|
|
19
|
+
id: 1,
|
|
20
|
+
category: 'growth',
|
|
21
|
+
category_name: '🚀 Growth & Striking Distance',
|
|
22
|
+
title: 'The Page 2 Striking Distance Leap',
|
|
23
|
+
impact: 'High Impact (Push Page 2 keywords to Top 3)',
|
|
24
|
+
cli_command: 'gsc opportunities --min-imp 20 --json',
|
|
25
|
+
template: 'Antigravity, run `gsc opportunities --min-imp 20 --json` for {{domain}}. Find the top 3 striking-distance queries (positions 8–18 with high impressions). For each query, locate its ranking page in our codebase, identify what content is missing compared to top SERP competitors, expand the page with a targeted FAQ and comparison table, and ping Google Indexing API via `gsc index <url>`.'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 2,
|
|
29
|
+
category: 'growth',
|
|
30
|
+
category_name: '🚀 Growth & Striking Distance',
|
|
31
|
+
title: 'The Untargeted Keyword Goldmine',
|
|
32
|
+
impact: 'High Impact (Create high-intent new landing pages)',
|
|
33
|
+
cli_command: 'gsc saved check 1 --json',
|
|
34
|
+
template: 'Antigravity, inspect our saved keyword research with `gsc saved check 1 --json`. Identify the top 5 keywords with Opportunity Score > 65 that are currently flagged as 🚀 Untargeted. For each keyword, propose a dedicated landing page route, draft high-intent metadata, and generate a semantic content outline.'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 3,
|
|
38
|
+
category: 'growth',
|
|
39
|
+
category_name: '🚀 Growth & Striking Distance',
|
|
40
|
+
title: '5-Year Google Trends Seasonal Surf',
|
|
41
|
+
impact: 'Medium Impact (Catch seasonal breakout demand)',
|
|
42
|
+
cli_command: 'gsc trends "{{seed}}" --time 5y --json',
|
|
43
|
+
template: 'Antigravity, run `gsc trends "{{seed}}" --time 5y --json`. Identify the historical peak search months, extract all rising breakout queries (+5000%), and update our landing page headings and marketing banners to capture the upcoming seasonal search surge.'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 4,
|
|
47
|
+
category: 'growth',
|
|
48
|
+
category_name: '🚀 Growth & Striking Distance',
|
|
49
|
+
title: 'Long-Tail Autocomplete Multiplier',
|
|
50
|
+
impact: 'Medium Impact (Target long-tail customer questions)',
|
|
51
|
+
cli_command: 'gsc planner "{{seed}}" --limit 50 --json',
|
|
52
|
+
template: 'Antigravity, run `gsc planner "{{seed}}" --limit 50 --json`. Group all discovered long-tail queries by search intent (Transactional vs Informational), and generate an interactive FAQ accordion component in our template addressing the top 5 customer questions.'
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
# Category: Conversion & Click-Through Rate (CTR)
|
|
56
|
+
{
|
|
57
|
+
id: 5,
|
|
58
|
+
category: 'ctr',
|
|
59
|
+
category_name: '🎯 Conversion & CTR Multipliers',
|
|
60
|
+
title: 'The CTR Underperformer Double',
|
|
61
|
+
impact: 'High Impact (Double organic clicks with 0 rank changes)',
|
|
62
|
+
cli_command: 'gsc underperformers --limit 5 --json',
|
|
63
|
+
template: 'Antigravity, run `gsc underperformers --limit 5 --json` for {{domain}}. For the page with the highest impressions but lowest CTR (<2%), read its current <title> and meta description in our codebase. Rewrite them applying the 50–60 character high-CTR formula (front-loading the exact query, adding bracketed hooks like [2026 Free Tool], and appending brand). Update the file and ping `gsc index <url>`.'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 6,
|
|
67
|
+
category: 'ctr',
|
|
68
|
+
category_name: '🎯 Conversion & CTR Multipliers',
|
|
69
|
+
title: 'Review & FAQ Rich Snippet Enabler',
|
|
70
|
+
impact: 'High Impact (Win star ratings and expandable FAQs in SERP)',
|
|
71
|
+
cli_command: 'gsc snippets --json',
|
|
72
|
+
template: 'Antigravity, run `gsc snippets --json` on {{domain}} to check active search appearances. Then inspect our top traffic pages from `gsc top-pages --json` and inject valid Schema.org JSON-LD structured data (FAQPage or Product) so our Google SERP listings display star ratings and expandable FAQs.'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 7,
|
|
76
|
+
category: 'ctr',
|
|
77
|
+
category_name: '🎯 Conversion & CTR Multipliers',
|
|
78
|
+
title: 'Headline & H1 Alignment Overhaul',
|
|
79
|
+
impact: 'Medium Impact (Increase on-page conversion rate)',
|
|
80
|
+
cli_command: 'gsc top-queries --limit 10 --json',
|
|
81
|
+
template: 'Antigravity, find our top 3 visited landing pages using `gsc ga4 --organic --json`. Audit their <h1> display headings against our copywriting rules (ensure zero trailing periods, strong benefit promise, and strict alignment with high-volume search queries from `gsc top-queries`).'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 8,
|
|
85
|
+
category: 'ctr',
|
|
86
|
+
category_name: '🎯 Conversion & CTR Multipliers',
|
|
87
|
+
title: 'Keyword Cannibalization Consolidator',
|
|
88
|
+
impact: 'High Impact (Stop competing against your own pages)',
|
|
89
|
+
cli_command: 'gsc cannibalization --json',
|
|
90
|
+
template: 'Antigravity, run `gsc cannibalization --json` on {{domain}}. Detect any search queries where 2 or more of our URLs are competing against each other and splitting Google impressions. Recommend which URL should be the authoritative canonical, and add cross-linking or 301 redirects to consolidate ranking power.'
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
# Category: GA4 Behavioral, Ads & Conversion Analytics
|
|
94
|
+
{
|
|
95
|
+
id: 9,
|
|
96
|
+
category: 'analytics',
|
|
97
|
+
category_name: '📊 GA4 Behavioral & Ad Synergy',
|
|
98
|
+
title: 'High-Bounce Traffic Leak Plugger',
|
|
99
|
+
impact: 'High Impact (Rescue lost visitors who bounce in <10s)',
|
|
100
|
+
cli_command: 'gsc correlation --json',
|
|
101
|
+
template: 'Antigravity, run `gsc correlation --json`. Correlate high-impression GSC search queries against GA4 bounce rates. Find search queries driving visitors who bounce in under 10 seconds. Audit the page\'s above-the-fold hero section and align the primary value proposition directly to user search intent.'
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 10,
|
|
105
|
+
category: 'analytics',
|
|
106
|
+
category_name: '📊 GA4 Behavioral & Ad Synergy',
|
|
107
|
+
title: 'Real-Time Traffic Wave Monitor',
|
|
108
|
+
impact: 'Medium Impact (Live visitor diagnostic & health check)',
|
|
109
|
+
cli_command: 'gsc realtime --json',
|
|
110
|
+
template: 'Antigravity, run `gsc realtime --json`. Check how many live visitors are currently on {{domain}}, which landing pages they are viewing, and verify that our conversion tracking and call-to-actions on those active pages are operating smoothly.'
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 11,
|
|
114
|
+
category: 'analytics',
|
|
115
|
+
category_name: '📊 GA4 Behavioral & Ad Synergy',
|
|
116
|
+
title: 'Google Ads & Organic Synergy Optimizer',
|
|
117
|
+
impact: 'High Impact (Cut wasted ad spend where you rank #1 organically)',
|
|
118
|
+
cli_command: 'gsc ads --json',
|
|
119
|
+
template: 'Antigravity, run `gsc ads --json` and `gsc top-queries --json`. Identify expensive Google Ads keywords (high CPC) where our site already ranks in Top 3 organically, and suggest pausing those paid ads to save ad spend while doubling down on organic CTR.'
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 12,
|
|
123
|
+
category: 'analytics',
|
|
124
|
+
category_name: '📊 GA4 Behavioral & Ad Synergy',
|
|
125
|
+
title: 'Omni-Channel Attribution & Engagement Audit',
|
|
126
|
+
impact: 'Medium Impact (Identify top converting traffic channels)',
|
|
127
|
+
cli_command: 'gsc channels --days 30 --json',
|
|
128
|
+
template: 'Antigravity, run `gsc channels --days 30 --json`. Compare organic search conversion rates against paid and direct traffic. Highlight which traffic channel has the highest customer engagement rate and recommend channel-specific landing page optimizations.'
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
# Category: Technical SEO, Crawl Health & Indexing
|
|
132
|
+
{
|
|
133
|
+
id: 13,
|
|
134
|
+
category: 'technical',
|
|
135
|
+
category_name: '🛠️ Technical Health & Crawl Optimization',
|
|
136
|
+
title: 'The 90-Day Zombie Page Purge',
|
|
137
|
+
impact: 'High Impact (Reclaim crawl budget and prune dead weight)',
|
|
138
|
+
cli_command: 'gsc zombies --json',
|
|
139
|
+
template: 'Antigravity, run `gsc zombies --json` on our sitemap. Identify all low-quality or obsolete pages that have received 0 impressions in the last 90 days. Recommend whether to update them with fresh content or retire them with `gsc remove <url>` to protect crawl budget.'
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 14,
|
|
143
|
+
category: 'technical',
|
|
144
|
+
category_name: '🛠️ Technical Health & Crawl Optimization',
|
|
145
|
+
title: 'Sitemap Coverage & Rapid Indexing Blitz',
|
|
146
|
+
impact: 'High Impact (Force Googlebot to index queued pages)',
|
|
147
|
+
cli_command: 'gsc inspect-sitemap https://{{domain}}/sitemap.xml --json',
|
|
148
|
+
template: 'Antigravity, run `gsc inspect-sitemap https://{{domain}}/sitemap.xml --json`. Identify all URLs marked as "Discovered - currently not indexed" or "Crawled - currently not indexed". For any unindexed URL, batch ping Google Indexing API via `gsc index <url>` to accelerate indexing.'
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 15,
|
|
152
|
+
category: 'technical',
|
|
153
|
+
category_name: '🛠️ Technical Health & Crawl Optimization',
|
|
154
|
+
title: 'Ranking Decay Early Warning Detection',
|
|
155
|
+
impact: 'High Impact (Arrest traffic loss before it accelerates)',
|
|
156
|
+
cli_command: 'gsc decay --days 28 --json',
|
|
157
|
+
template: 'Antigravity, run `gsc decay --days 28 --json`. Identify queries or pages experiencing period-over-period click or impression drops (>20%). Propose immediate content freshness updates and internal link boosts to reverse the decay.'
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 16,
|
|
161
|
+
category: 'technical',
|
|
162
|
+
category_name: '🛠️ Technical Health & Crawl Optimization',
|
|
163
|
+
title: 'Lost Query Resuscitation',
|
|
164
|
+
impact: 'Medium Impact (Recover queries that fell out of Google)',
|
|
165
|
+
cli_command: 'gsc decay --json',
|
|
166
|
+
template: 'Antigravity, run `gsc decay --json` and filter by "lost". Find high-volume keywords that generated traffic last month but completely dropped off this month. Inspect the previous URL and restore missing topical sections.'
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
# Category: Programmatic SEO & Scalable Architecture
|
|
170
|
+
{
|
|
171
|
+
id: 17,
|
|
172
|
+
category: 'programmatic',
|
|
173
|
+
category_name: '⚡ Programmatic SEO & Directory Scaling',
|
|
174
|
+
title: 'Programmatic Landing Page Generator',
|
|
175
|
+
impact: 'High Impact (Build 50+ data-driven landing pages)',
|
|
176
|
+
cli_command: 'gsc saved check 1 --json',
|
|
177
|
+
template: 'Antigravity, run `gsc saved check 1 --json`. Extract the top 10 untargeted city or feature keywords. Generate a reusable programmatic template that renders unique, value-dense content for each variation without creating duplicate content.'
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 18,
|
|
181
|
+
category: 'programmatic',
|
|
182
|
+
category_name: '⚡ Programmatic SEO & Directory Scaling',
|
|
183
|
+
title: 'Competitor Gap Exploitation via Import',
|
|
184
|
+
impact: 'High Impact (Steal competitor high-volume terms)',
|
|
185
|
+
cli_command: 'gsc import clip --json',
|
|
186
|
+
template: 'Antigravity, run `gsc import clip --json` using our latest competitor export from clipboard. Compare their highest volume keywords against our `gsc top-queries --json`. Identify the 5 most profitable keywords where our competitor ranks but we have zero presence.'
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 19,
|
|
190
|
+
category: 'programmatic',
|
|
191
|
+
category_name: '⚡ Programmatic SEO & Directory Scaling',
|
|
192
|
+
title: 'Zero-Click Search & AI Overview Winning Strategy',
|
|
193
|
+
impact: 'High Impact (Win citations in Google AI Overviews)',
|
|
194
|
+
cli_command: 'gsc top-queries -s imp --limit 20 --json',
|
|
195
|
+
template: 'Antigravity, run `gsc top-queries -s imp --limit 20 --json`. Identify informational queries where Google shows AI Overviews or direct answer boxes. Structure our content with concise 40-word definitions, numbered steps, and comparison tables to win the AI Overview citation.'
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 20,
|
|
199
|
+
category: 'programmatic',
|
|
200
|
+
category_name: '⚡ Programmatic SEO & Directory Scaling',
|
|
201
|
+
title: 'Mobile vs Desktop SERP Parity Audit',
|
|
202
|
+
impact: 'Medium Impact (Fix mobile ranking discrepancies)',
|
|
203
|
+
cli_command: 'gsc devices --json',
|
|
204
|
+
template: 'Antigravity, run `gsc devices --json`. Compare Mobile CTR vs Desktop CTR for {{domain}}. If mobile CTR lags by more than 30%, inspect our mobile viewport layouts, tap target sizes, and above-the-fold content density.'
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
# Category: Executive Briefings & Daily Standups
|
|
208
|
+
{
|
|
209
|
+
id: 21,
|
|
210
|
+
category: 'executive',
|
|
211
|
+
category_name: '📋 Executive Briefings & Standups',
|
|
212
|
+
title: '360° Executive SEO Health Briefing',
|
|
213
|
+
impact: 'High Impact (Complete C-Suite progress report)',
|
|
214
|
+
cli_command: 'gsc performance --days 30 --json',
|
|
215
|
+
template: 'Antigravity, run `gsc performance --days 30 --json`, `gsc decay --json`, and `gsc opportunities --json`. Generate a markdown briefing summarizing: Total Clicks & Growth % MoM, Top 3 Emerging Keywords, Top 3 Striking-Distance Opportunities, and 3 Critical Action Items for this sprint.'
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 22,
|
|
219
|
+
category: 'executive',
|
|
220
|
+
category_name: '📋 Executive Briefings & Standups',
|
|
221
|
+
title: 'New Feature Launch Indexing Blitz',
|
|
222
|
+
impact: 'High Impact (Index new releases within hours)',
|
|
223
|
+
cli_command: 'gsc index {{url}} --json',
|
|
224
|
+
template: 'Antigravity, I just launched a new feature/landing page at {{url}}. Inspect its metadata, verify Schema markup, confirm robots.txt accessibility, and immediately submit it to Google Indexing API via `gsc index {{url}}`.'
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 23,
|
|
228
|
+
category: 'executive',
|
|
229
|
+
category_name: '📋 Executive Briefings & Standups',
|
|
230
|
+
title: 'Geographic Market Expansion Diagnostic',
|
|
231
|
+
impact: 'Medium Impact (Find international expansion markets)',
|
|
232
|
+
cli_command: 'gsc countries --limit 20 --json',
|
|
233
|
+
template: 'Antigravity, run `gsc countries --limit 20 --json` and `gsc cities --limit 20 --json`. Identify our top 3 international or regional markets outside our primary country. Check if localized currency, language, or shipping details are needed on those landing pages.'
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 24,
|
|
237
|
+
category: 'executive',
|
|
238
|
+
category_name: '📋 Executive Briefings & Standups',
|
|
239
|
+
title: 'Title Tag Pixel Width & Truncation Audit',
|
|
240
|
+
impact: 'Medium Impact (Prevent Google ... truncation on all pages)',
|
|
241
|
+
cli_command: 'gsc top-pages --limit 25 --json',
|
|
242
|
+
template: 'Antigravity, crawl our sitemap via `gsc inspect-sitemap --json`, extract all page <title> tags from our repository, and flag any titles under 40 characters (too short) or over 60 characters (truncated by Google with ...). Propose rewritten versions for all flagged titles.'
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 25,
|
|
246
|
+
category: 'executive',
|
|
247
|
+
category_name: '📋 Executive Briefings & Standups',
|
|
248
|
+
title: 'The Daily 5-Minute SEO Standup',
|
|
249
|
+
impact: 'High Impact (Daily check of pulses, wins, and anomalies)',
|
|
250
|
+
cli_command: 'gsc realtime --json',
|
|
251
|
+
template: 'Antigravity, run `gsc realtime --json`, `gsc top-queries --limit 5 --json`, and `gsc decay --json`. Give me a 3-bullet standup: (1) Live visitors right now, (2) Yesterday\'s top performing search query, and (3) Any query that saw an unexpected drop requiring attention.'
|
|
252
|
+
}
|
|
253
|
+
].freeze
|
|
254
|
+
|
|
255
|
+
def self.all
|
|
256
|
+
PLAYBOOKS
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def self.find(id)
|
|
260
|
+
PLAYBOOKS.find { |p| p[:id] == id.to_i }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def self.by_category(cat)
|
|
264
|
+
PLAYBOOKS.select { |p| p[:category] == cat.to_s.downcase }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def self.categories
|
|
268
|
+
PLAYBOOKS.map { |p| { id: p[:category], name: p[:category_name] } }.uniq { |c| c[:id] }
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def self.render_prompt(id, domain: nil, seed: nil, url: nil)
|
|
272
|
+
item = find(id)
|
|
273
|
+
return nil unless item
|
|
274
|
+
|
|
275
|
+
dom = domain || Config.default_domain || 'example.com'
|
|
276
|
+
s = seed || 'moving boxes'
|
|
277
|
+
u = url || "https://#{dom}"
|
|
278
|
+
|
|
279
|
+
item[:template]
|
|
280
|
+
.gsub('{{domain}}', dom)
|
|
281
|
+
.gsub('{{seed}}', s)
|
|
282
|
+
.gsub('{{url}}', u)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
|
|
9
|
+
module GSC
|
|
10
|
+
class SiteCrawler
|
|
11
|
+
attr_reader :target, :options, :results, :broken_links, :missing_alts, :heading_issues, :title_issues, :canonical_issues
|
|
12
|
+
|
|
13
|
+
def initialize(target, options = {})
|
|
14
|
+
@target = target.to_s.strip
|
|
15
|
+
@options = options
|
|
16
|
+
@results = []
|
|
17
|
+
@broken_links = []
|
|
18
|
+
@missing_alts = []
|
|
19
|
+
@heading_issues = []
|
|
20
|
+
@title_issues = []
|
|
21
|
+
@canonical_issues = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run(&progress_block)
|
|
25
|
+
urls = discover_urls(@target)
|
|
26
|
+
urls = urls.first(@options[:limit]) if @options[:limit] && @options[:limit] > 0
|
|
27
|
+
|
|
28
|
+
total = urls.size
|
|
29
|
+
urls.each_with_index do |url, idx|
|
|
30
|
+
progress_block.call(url, idx + 1, total) if block_given?
|
|
31
|
+
|
|
32
|
+
analyzer = PageAnalyzer.new(url)
|
|
33
|
+
page_data = analyzer.fetch_and_analyze(
|
|
34
|
+
check_links: @options[:check_links] || false,
|
|
35
|
+
gsc_api: @options[:gsc_api],
|
|
36
|
+
active_domain: @options[:active_domain]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@results << page_data
|
|
40
|
+
categorize_page_issues(page_data)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
aggregate_summary
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def generate_markdown_report(filepath)
|
|
47
|
+
summary = aggregate_summary
|
|
48
|
+
FileUtils.mkdir_p(File.dirname(File.expand_path(filepath)))
|
|
49
|
+
|
|
50
|
+
md = []
|
|
51
|
+
md << "# 🛠️ Autonomous SEO Site Audit & Broken Link Report"
|
|
52
|
+
md << ""
|
|
53
|
+
md << "> **Target**: `#{@target}` "
|
|
54
|
+
md << "> **Audit Date**: `#{Time.now.strftime('%Y-%m-%d %H:%M:%S UTC')}` "
|
|
55
|
+
md << "> **Pages Audited**: `#{summary[:total_pages]}` | **Total Issues Found**: `#{summary[:total_issues]}` "
|
|
56
|
+
md << "> **Broken Links (404/Error)**: `#{summary[:broken_links_count]}` | **Missing Alt Images**: `#{summary[:missing_alts_count]}` "
|
|
57
|
+
md << ""
|
|
58
|
+
md << "---"
|
|
59
|
+
md << ""
|
|
60
|
+
md << "## 📊 1. Executive Health Scorecard"
|
|
61
|
+
md << ""
|
|
62
|
+
md << "| Metric | Count / Value | Health Status |"
|
|
63
|
+
md << "| :--- | :---: | :--- |"
|
|
64
|
+
md << "| **Total Pages Audited** | `#{summary[:total_pages]}` | ℹ️ Crawl Scope |"
|
|
65
|
+
md << "| **Critical Errors** | `#{summary[:critical_errors_count]}` | #{summary[:critical_errors_count] == 0 ? '🟢 Clean' : '🔴 Action Required'} |"
|
|
66
|
+
md << "| **Broken Links (404/500)** | `#{summary[:broken_links_count]}` | #{summary[:broken_links_count] == 0 ? '🟢 Zero Broken Links' : '🔴 Broken Links Detected'} |"
|
|
67
|
+
md << "| **Images Missing Alt** | `#{summary[:missing_alts_count]}` | #{summary[:missing_alts_count] == 0 ? '🟢 100% Accessible' : '🟡 Needs Alt Text'} |"
|
|
68
|
+
md << "| **Pages with 0 or >1 H1** | `#{summary[:heading_issues_count]}` | #{summary[:heading_issues_count] == 0 ? '🟢 Perfect Hierarchy' : '🟡 Review H1s'} |"
|
|
69
|
+
md << "| **Title / Meta Flaws** | `#{summary[:title_meta_issues_count]}` | #{summary[:title_meta_issues_count] == 0 ? '🟢 Optimal' : '🟡 Review SERP Truncation'} |"
|
|
70
|
+
md << ""
|
|
71
|
+
md << "---"
|
|
72
|
+
md << ""
|
|
73
|
+
|
|
74
|
+
# 2. Broken Links
|
|
75
|
+
md << "## 🚨 2. Broken Links & Dead Anchors (#{summary[:broken_links_count]} Found)"
|
|
76
|
+
md << ""
|
|
77
|
+
if @broken_links.empty?
|
|
78
|
+
md << "✅ **No broken links detected across all audited pages!**"
|
|
79
|
+
else
|
|
80
|
+
md << "The following links returned HTTP errors (404 Not Found, 500 Server Error, or Timeout) and should be updated or removed:"
|
|
81
|
+
md << ""
|
|
82
|
+
md << "| Source Page URL | Target Broken URL | Anchor Text | HTTP Status |"
|
|
83
|
+
md << "| :--- | :--- | :--- | :---: |"
|
|
84
|
+
@broken_links.each do |b|
|
|
85
|
+
md << "| `#{b[:source_page]}` | `#{b[:href]}` | #{b[:anchor].empty? ? '*(Empty)*' : b[:anchor]} | **#{b[:status]}** |"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
md << ""
|
|
89
|
+
md << "---"
|
|
90
|
+
md << ""
|
|
91
|
+
|
|
92
|
+
# 3. Image Alt Tag Fixes
|
|
93
|
+
md << "## 🖼️ 3. Images Missing Alt Attributes (#{summary[:missing_alts_count]} Found)"
|
|
94
|
+
md << ""
|
|
95
|
+
if @missing_alts.empty?
|
|
96
|
+
md << "✅ **All images on audited pages have descriptive alt text!**"
|
|
97
|
+
else
|
|
98
|
+
md << "Search engines and screen readers rely on descriptive `alt` attributes to index visual content:"
|
|
99
|
+
md << ""
|
|
100
|
+
md << "| Page URL | Image Source URL | Recommended Fix |"
|
|
101
|
+
md << "| :--- | :--- | :--- |"
|
|
102
|
+
@missing_alts.each do |img|
|
|
103
|
+
md << "| `#{img[:page_url]}` | `#{img[:src]}` | Add descriptive keywords to `alt=\"...\"` |"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
md << ""
|
|
107
|
+
md << "---"
|
|
108
|
+
md << ""
|
|
109
|
+
|
|
110
|
+
# 4. Heading Hierarchy Flaws
|
|
111
|
+
md << "## 📑 4. Heading Hierarchy & H1 Flaws (#{summary[:heading_issues_count]} Found)"
|
|
112
|
+
md << ""
|
|
113
|
+
if @heading_issues.empty?
|
|
114
|
+
md << "✅ **All pages have exactly one <h1> and clean structure!**"
|
|
115
|
+
else
|
|
116
|
+
md << "| Page URL | Issue Details | Recommended Action |"
|
|
117
|
+
md << "| :--- | :--- | :--- |"
|
|
118
|
+
@heading_issues.each do |h|
|
|
119
|
+
md << "| `#{h[:page_url]}` | #{h[:issue]} | Ensure exactly one <h1> matching primary search query |"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
md << ""
|
|
123
|
+
md << "---"
|
|
124
|
+
md << ""
|
|
125
|
+
|
|
126
|
+
# 5. Title & Meta Description Flaws
|
|
127
|
+
md << "## 🏷️ 5. Title & Meta Description Optimizations (#{summary[:title_meta_issues_count]} Found)"
|
|
128
|
+
md << ""
|
|
129
|
+
if @title_issues.empty?
|
|
130
|
+
md << "✅ **All titles and meta descriptions meet 30–60 char and 70–155 char standards!**"
|
|
131
|
+
else
|
|
132
|
+
md << "| Page URL | Current Title (Chars) | Current Meta (Chars) | Flaw Detected |"
|
|
133
|
+
md << "| :--- | :--- | :--- | :--- |"
|
|
134
|
+
@title_issues.each do |t|
|
|
135
|
+
md << "| `#{t[:page_url]}` | #{t[:title]} (#{t[:title_chars]}c) | #{t[:meta]} (#{t[:meta_chars]}c) | #{t[:flaw]} |"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
md << ""
|
|
139
|
+
md << "---"
|
|
140
|
+
md << ""
|
|
141
|
+
|
|
142
|
+
# 6. Prioritized AI Action Sprint
|
|
143
|
+
md << "## 🤖 6. AI Agent Automated Fix Sprint"
|
|
144
|
+
md << ""
|
|
145
|
+
md << "Use these instructions to locate template files and apply fixes:"
|
|
146
|
+
md << ""
|
|
147
|
+
md << "1. **P0: Fix Broken Links**: Locate `<a href=\"...\">` tags pointing to dead URLs identified in Section 2."
|
|
148
|
+
md << "2. **P0: Single <h1> Enforcement**: Ensure all template layouts have exactly one `<h1>`."
|
|
149
|
+
md << "3. **P1: Image Alt Tag Insertion**: Add descriptive `alt` attributes to all images in Section 3."
|
|
150
|
+
md << "4. **P1: Title Truncation Fix**: Keep `<title>` under 60 characters and `<meta name=\"description\">` under 155 characters."
|
|
151
|
+
md << "5. **P2: Googlebot Re-Index**: Ping Google's Indexing API for all updated URLs via `gsc index <url>`."
|
|
152
|
+
md << ""
|
|
153
|
+
md << "---\n*Report generated by `gsc site-audit` (On-Page & Off-Page SEO Engine)*"
|
|
154
|
+
|
|
155
|
+
File.write(filepath, md.join("\n"), encoding: 'UTF-8')
|
|
156
|
+
filepath
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def aggregate_summary
|
|
160
|
+
critical_errors = @broken_links.size + @results.count { |r| r[:indexability][:noindex] }
|
|
161
|
+
total_issues = critical_errors + @missing_alts.size + @heading_issues.size + @title_issues.size + @canonical_issues.size
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
total_pages: @results.size,
|
|
165
|
+
total_issues: total_issues,
|
|
166
|
+
critical_errors_count: critical_errors,
|
|
167
|
+
broken_links_count: @broken_links.size,
|
|
168
|
+
missing_alts_count: @missing_alts.size,
|
|
169
|
+
heading_issues_count: @heading_issues.size,
|
|
170
|
+
title_meta_issues_count: @title_issues.size,
|
|
171
|
+
canonical_issues_count: @canonical_issues.size
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def discover_urls(target)
|
|
178
|
+
if target.end_with?('.xml') || target.include?('sitemap')
|
|
179
|
+
SitemapLoader.load_urls(target)
|
|
180
|
+
elsif File.file?(target)
|
|
181
|
+
[target]
|
|
182
|
+
else
|
|
183
|
+
normalized = target.start_with?('http') ? target : "https://#{target}"
|
|
184
|
+
sitemap_url = "#{normalized.sub(%r{/+$}, '')}/sitemap.xml"
|
|
185
|
+
urls = SitemapLoader.load_urls(sitemap_url)
|
|
186
|
+
urls.empty? ? [normalized] : urls
|
|
187
|
+
end
|
|
188
|
+
rescue StandardError
|
|
189
|
+
[target.start_with?('http') ? target : "https://#{target}"]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def categorize_page_issues(data)
|
|
193
|
+
page_url = data[:url]
|
|
194
|
+
|
|
195
|
+
# Broken links
|
|
196
|
+
if data.dig(:links, :verification)
|
|
197
|
+
data[:links][:verification].each do |link|
|
|
198
|
+
if !link[:ok]
|
|
199
|
+
@broken_links << {
|
|
200
|
+
source_page: page_url,
|
|
201
|
+
href: link[:href],
|
|
202
|
+
anchor: link[:anchor],
|
|
203
|
+
status: link[:status] || 'Error'
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Missing alts
|
|
210
|
+
if data.dig(:images, :missing_alt_images)
|
|
211
|
+
data[:images][:missing_alt_images].each do |img|
|
|
212
|
+
@missing_alts << {
|
|
213
|
+
page_url: page_url,
|
|
214
|
+
src: img[:src]
|
|
215
|
+
}
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Headings
|
|
220
|
+
h1_count = data.dig(:headings, :h1_count) || 0
|
|
221
|
+
if h1_count == 0
|
|
222
|
+
@heading_issues << { page_url: page_url, issue: "Missing <h1> tag (0 found)" }
|
|
223
|
+
elsif h1_count > 1
|
|
224
|
+
@heading_issues << { page_url: page_url, issue: "Multiple <h1> tags (#{h1_count} found)" }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Title & Meta
|
|
228
|
+
title_chars = data.dig(:title, :length) || 0
|
|
229
|
+
meta_chars = data.dig(:meta_description, :length) || 0
|
|
230
|
+
title_text = data.dig(:title, :text) || ''
|
|
231
|
+
meta_text = data.dig(:meta_description, :text) || ''
|
|
232
|
+
|
|
233
|
+
flaws = []
|
|
234
|
+
flaws << "Title > 60 chars" if title_chars > 60
|
|
235
|
+
flaws << "Title < 30 chars" if title_chars > 0 && title_chars < 30
|
|
236
|
+
flaws << "Missing Title" if title_chars == 0
|
|
237
|
+
flaws << "Meta > 155 chars" if meta_chars > 155
|
|
238
|
+
flaws << "Missing Meta" if meta_chars == 0
|
|
239
|
+
|
|
240
|
+
if !flaws.empty?
|
|
241
|
+
@title_issues << {
|
|
242
|
+
page_url: page_url,
|
|
243
|
+
title: title_text[0..40],
|
|
244
|
+
title_chars: title_chars,
|
|
245
|
+
meta: meta_text[0..40],
|
|
246
|
+
meta_chars: meta_chars,
|
|
247
|
+
flaw: flaws.join(', ')
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Canonical
|
|
252
|
+
if data.dig(:canonical, :url) && !data.dig(:canonical, :self_referencing)
|
|
253
|
+
@canonical_issues << {
|
|
254
|
+
page_url: page_url,
|
|
255
|
+
canonical_url: data[:canonical][:url]
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'zlib'
|
|
6
|
+
require 'stringio'
|
|
7
|
+
|
|
8
|
+
module GSC
|
|
9
|
+
class SitemapLoader
|
|
10
|
+
def self.fetch_content(path_or_url)
|
|
11
|
+
if path_or_url.start_with?('http://', 'https://')
|
|
12
|
+
uri = URI(path_or_url)
|
|
13
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
14
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
15
|
+
http.open_timeout = 10
|
|
16
|
+
http.read_timeout = 20
|
|
17
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
18
|
+
req['User-Agent'] = 'Mozilla/5.0 (compatible; GSC-SEO-Auditor/1.0)'
|
|
19
|
+
req['Accept-Encoding'] = 'gzip'
|
|
20
|
+
|
|
21
|
+
res = http.request(req)
|
|
22
|
+
raise "HTTP #{res.code} fetching sitemap: #{path_or_url}" unless res.is_a?(Net::HTTPSuccess)
|
|
23
|
+
|
|
24
|
+
raw = res.body || ''
|
|
25
|
+
decompressed = if res['content-encoding'] =~ /gzip/i && !raw.empty?
|
|
26
|
+
Zlib::GzipReader.new(StringIO.new(raw)).read
|
|
27
|
+
else
|
|
28
|
+
raw
|
|
29
|
+
end
|
|
30
|
+
decompressed.to_s.dup.force_encoding('UTF-8').scrub
|
|
31
|
+
else
|
|
32
|
+
raise "Local file not found: #{path_or_url}" unless File.exist?(path_or_url)
|
|
33
|
+
File.read(path_or_url, encoding: 'UTF-8').scrub
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.load_urls(input, default_origin = nil)
|
|
38
|
+
resolve_urls(input, default_origin, quiet: true)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.resolve_urls(input, default_origin = nil, quiet: false)
|
|
42
|
+
target_input = input
|
|
43
|
+
if target_input.nil? || target_input.empty?
|
|
44
|
+
candidates = [
|
|
45
|
+
'public/sitemap.xml',
|
|
46
|
+
'public/sitemap-0.xml',
|
|
47
|
+
'public/sitemap-index.xml',
|
|
48
|
+
'dist/sitemap.xml',
|
|
49
|
+
'dist/sitemap-index.xml'
|
|
50
|
+
]
|
|
51
|
+
found = candidates.find { |f| File.exist?(f) }
|
|
52
|
+
if found
|
|
53
|
+
target_input = found
|
|
54
|
+
puts Color.c("📄 Auto-detected local sitemap: #{found}", Color::GRAY) unless quiet
|
|
55
|
+
elsif default_origin
|
|
56
|
+
target_input = "#{default_origin.chomp('/')}/sitemap.xml"
|
|
57
|
+
puts Color.c("🌐 Using remote sitemap URL: #{target_input}", Color::GRAY) unless quiet
|
|
58
|
+
else
|
|
59
|
+
raise 'No sitemap file or URL provided, and no local sitemap found.'
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
xml = fetch_content(target_input)
|
|
64
|
+
urls = []
|
|
65
|
+
|
|
66
|
+
# Parse child sitemaps if this is a sitemap index
|
|
67
|
+
sitemap_locs = xml.scan(/<sitemap>\s*<loc>([^<]+)<\/loc>/m).flatten
|
|
68
|
+
if sitemap_locs.any?
|
|
69
|
+
puts "📑 Found #{sitemap_locs.size} nested sitemaps in index..." unless quiet
|
|
70
|
+
sitemap_locs.each do |child_url|
|
|
71
|
+
child_url = child_url.strip
|
|
72
|
+
unless child_url.start_with?('http://', 'https://')
|
|
73
|
+
puts Color.c(" ⚠️ Skipping non-HTTP child sitemap location: #{child_url}", Color::YELLOW) unless quiet
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
puts " ↳ Loading child sitemap: #{child_url}" unless quiet
|
|
77
|
+
begin
|
|
78
|
+
child_xml = fetch_content(child_url)
|
|
79
|
+
urls.concat(child_xml.scan(/<url>\s*<loc>([^<]+)<\/loc>/m).flatten.map(&:strip))
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
puts Color.c(" ⚠️ Warning: Could not load child sitemap #{child_url}: #{e.message}", Color::YELLOW) unless quiet
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
urls.concat(xml.scan(/<loc>([^<]+)<\/loc>/m).flatten.map(&:strip))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
urls.uniq
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|