gscdump 0.1.3 → 0.3.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.
package/dist/index.mjs CHANGED
@@ -1,649 +1,5 @@
1
- import { withBase, withHttps, withoutTrailingSlash } from "ufo";
2
- import _dayjs from "dayjs";
3
- import timezone from "dayjs/plugin/timezone.js";
4
- import utc from "dayjs/plugin/utc.js";
5
1
  import { ofetch } from "ofetch";
6
2
 
7
- //#region src/analysis/types.ts
8
- /** Coerce nullable number to number, defaulting to 0 */
9
- function num(value) {
10
- return value ?? 0;
11
- }
12
- /** Create a generic sorter for any metric type */
13
- function createSorter(getValue, defaultMetric, defaultOrder = "desc") {
14
- return (items, sortBy = defaultMetric, sortOrder = defaultOrder) => {
15
- const mult = sortOrder === "desc" ? -1 : 1;
16
- return [...items].sort((a, b) => (getValue(a, sortBy) - getValue(b, sortBy)) * mult);
17
- };
18
- }
19
-
20
- //#endregion
21
- //#region src/analysis/brand.ts
22
- /**
23
- * Segments keywords into brand and non-brand based on provided brand terms.
24
- * Simple string matching - keyword contains any brand term (case-insensitive).
25
- */
26
- function analyzeBrandSegmentation(keywords, options) {
27
- const { brandTerms, minImpressions = 10 } = options;
28
- const lowerBrandTerms = brandTerms.map((t) => t.toLowerCase());
29
- const brand = [];
30
- const nonBrand = [];
31
- for (const row of keywords) {
32
- if (num(row.impressions) < minImpressions) continue;
33
- if (lowerBrandTerms.some((term) => row.keyword.toLowerCase().includes(term))) brand.push(row);
34
- else nonBrand.push(row);
35
- }
36
- const brandClicks = brand.reduce((sum, k) => sum + num(k.clicks), 0);
37
- const nonBrandClicks = nonBrand.reduce((sum, k) => sum + num(k.clicks), 0);
38
- const totalClicks = brandClicks + nonBrandClicks;
39
- return {
40
- brand,
41
- nonBrand,
42
- summary: {
43
- brandClicks,
44
- nonBrandClicks,
45
- brandShare: totalClicks > 0 ? brandClicks / totalClicks : 0,
46
- brandImpressions: brand.reduce((sum, k) => sum + num(k.impressions), 0),
47
- nonBrandImpressions: nonBrand.reduce((sum, k) => sum + num(k.impressions), 0)
48
- }
49
- };
50
- }
51
-
52
- //#endregion
53
- //#region src/analysis/cannibalization.ts
54
- const sortResults$4 = createSorter((item, metric) => {
55
- switch (metric) {
56
- case "clicks": return item.totalClicks;
57
- case "impressions": return item.totalImpressions;
58
- case "positionSpread": return item.positionSpread;
59
- case "pageCount": return item.pages.length;
60
- }
61
- }, "clicks");
62
- /**
63
- * Detects keyword cannibalization - queries ranking for multiple pages.
64
- * Returns queries where multiple pages compete for the same search term.
65
- *
66
- * @param rows Query+page data rows
67
- * @param options Filtering and sorting options
68
- */
69
- function analyzeCannibalization(rows, options = {}) {
70
- const { minImpressions = 10, maxPositionSpread = 10, minPages = 2, sortBy = "clicks", sortOrder = "desc" } = options;
71
- const queryMap = /* @__PURE__ */ new Map();
72
- for (const row of rows) {
73
- if (row.impressions < minImpressions) continue;
74
- const pages = queryMap.get(row.query) || [];
75
- pages.push({
76
- page: row.page,
77
- clicks: row.clicks,
78
- impressions: row.impressions,
79
- ctr: row.ctr,
80
- position: row.position
81
- });
82
- queryMap.set(row.query, pages);
83
- }
84
- const results = [];
85
- for (const [query$1, pages] of queryMap) {
86
- if (pages.length < minPages) continue;
87
- pages.sort((a, b) => b.clicks - a.clicks);
88
- const positions = pages.map((p) => p.position);
89
- const positionSpread = Math.max(...positions) - Math.min(...positions);
90
- if (positionSpread > maxPositionSpread) continue;
91
- results.push({
92
- query: query$1,
93
- pages,
94
- totalClicks: pages.reduce((sum, p) => sum + p.clicks, 0),
95
- totalImpressions: pages.reduce((sum, p) => sum + p.impressions, 0),
96
- positionSpread
97
- });
98
- }
99
- return sortResults$4(results, sortBy, sortOrder);
100
- }
101
-
102
- //#endregion
103
- //#region src/analysis/clustering.ts
104
- const INTENT_PREFIXES = [
105
- "how to",
106
- "what is",
107
- "what are",
108
- "why is",
109
- "why do",
110
- "where to",
111
- "when to",
112
- "best",
113
- "top",
114
- "vs",
115
- "versus",
116
- "compare",
117
- "review",
118
- "buy",
119
- "cheap",
120
- "free",
121
- "near me"
122
- ];
123
- function extractIntentPrefix(keyword) {
124
- const lower = keyword.toLowerCase();
125
- for (const prefix of INTENT_PREFIXES) if (lower.startsWith(`${prefix} `) || lower.startsWith(prefix)) return prefix;
126
- return null;
127
- }
128
- function extractWordPrefix(keyword, wordCount = 2) {
129
- const words = keyword.toLowerCase().split(/\s+/).filter(Boolean);
130
- if (words.length < wordCount + 1) return null;
131
- return words.slice(0, wordCount).join(" ");
132
- }
133
- /**
134
- * Clusters keywords by intent prefix or common word prefix.
135
- * Simple regex/prefix approach - no external NLP dependencies.
136
- *
137
- * @param keywords Array of keyword data
138
- * @param options Clustering options
139
- */
140
- function analyzeClustering(keywords, options = {}) {
141
- const { minClusterSize = 2, minImpressions = 10, clusterBy = "both" } = options;
142
- const filtered = keywords.filter((k) => num(k.impressions) >= minImpressions);
143
- const clusterMap = /* @__PURE__ */ new Map();
144
- const clusteredKeywords = /* @__PURE__ */ new Set();
145
- if (clusterBy === "intent" || clusterBy === "both") for (const kw of filtered) {
146
- const intent = extractIntentPrefix(kw.keyword);
147
- if (intent) {
148
- const existing = clusterMap.get(intent);
149
- if (existing) existing.keywords.push(kw);
150
- else clusterMap.set(intent, {
151
- type: "intent",
152
- keywords: [kw]
153
- });
154
- clusteredKeywords.add(kw.keyword);
155
- }
156
- }
157
- if (clusterBy === "prefix" || clusterBy === "both") {
158
- const unclustered = filtered.filter((kw) => !clusteredKeywords.has(kw.keyword));
159
- const prefixMap = /* @__PURE__ */ new Map();
160
- for (const kw of unclustered) {
161
- const prefix = extractWordPrefix(kw.keyword);
162
- if (prefix) {
163
- const existing = prefixMap.get(prefix);
164
- if (existing) existing.push(kw);
165
- else prefixMap.set(prefix, [kw]);
166
- }
167
- }
168
- for (const [prefix, kws] of prefixMap) if (kws.length >= minClusterSize) {
169
- clusterMap.set(prefix, {
170
- type: "prefix",
171
- keywords: kws
172
- });
173
- kws.forEach((kw) => clusteredKeywords.add(kw.keyword));
174
- }
175
- }
176
- const clusters = [];
177
- for (const [name, data] of clusterMap) {
178
- if (data.keywords.length < minClusterSize) continue;
179
- const totalClicks = data.keywords.reduce((sum, k) => sum + num(k.clicks), 0);
180
- const totalImpressions = data.keywords.reduce((sum, k) => sum + num(k.impressions), 0);
181
- const avgPosition = data.keywords.reduce((sum, k) => sum + num(k.position), 0) / data.keywords.length;
182
- clusters.push({
183
- clusterName: name,
184
- clusterType: data.type,
185
- keywords: data.keywords,
186
- totalClicks,
187
- totalImpressions,
188
- avgPosition,
189
- keywordCount: data.keywords.length
190
- });
191
- }
192
- clusters.sort((a, b) => b.totalClicks - a.totalClicks);
193
- return {
194
- clusters,
195
- unclustered: filtered.filter((kw) => !clusteredKeywords.has(kw.keyword))
196
- };
197
- }
198
-
199
- //#endregion
200
- //#region src/analysis/concentration.ts
201
- /**
202
- * Traffic concentration analysis - measures distribution across pages/keywords.
203
- * Pure function operating on page or keyword data.
204
- */
205
- /**
206
- * Calculates Gini coefficient for a list of values.
207
- * Formula: sum((2*i - n - 1) * x_i) / (n * sum(x_i))
208
- */
209
- function calculateGini(values) {
210
- if (values.length === 0) return 0;
211
- const sorted = [...values].sort((a, b) => a - b);
212
- const n = sorted.length;
213
- const sum = sorted.reduce((a, b) => a + b, 0);
214
- if (sum === 0) return 0;
215
- let weightedSum = 0;
216
- for (let i = 0; i < n; i++) weightedSum += (2 * (i + 1) - n - 1) * sorted[i];
217
- return weightedSum / (n * sum);
218
- }
219
- /**
220
- * Calculates Herfindahl-Hirschman Index (HHI).
221
- * Formula: sum((share_i * 100)^2)
222
- * Range: 0-10000 (0 = perfect competition, 10000 = monopoly)
223
- */
224
- function calculateHHI(shares) {
225
- return shares.reduce((sum, share) => sum + (share * 100) ** 2, 0);
226
- }
227
- /**
228
- * Analyzes traffic concentration across items (pages or keywords).
229
- * Returns Gini coefficient, HHI, and top-N concentration metrics.
230
- *
231
- * @param items Array of items with key and clicks
232
- * @param options Configuration options
233
- */
234
- function analyzeConcentration(items, options = {}) {
235
- const { topN = 10 } = options;
236
- if (items.length === 0) return {
237
- giniCoefficient: 0,
238
- hhi: 0,
239
- topNConcentration: 0,
240
- topNItems: [],
241
- totalItems: 0,
242
- totalClicks: 0,
243
- riskLevel: "low"
244
- };
245
- const sorted = [...items].sort((a, b) => b.clicks - a.clicks);
246
- const totalClicks = sorted.reduce((sum, item) => sum + item.clicks, 0);
247
- const clickValues = sorted.map((i) => i.clicks);
248
- const shares = totalClicks > 0 ? sorted.map((i) => i.clicks / totalClicks) : [];
249
- const giniCoefficient = calculateGini(clickValues);
250
- const hhi = calculateHHI(shares);
251
- const topNItems = sorted.slice(0, topN).map((item) => ({
252
- key: item.key,
253
- clicks: item.clicks,
254
- share: totalClicks > 0 ? item.clicks / totalClicks : 0
255
- }));
256
- const topNClicks = topNItems.reduce((sum, item) => sum + item.clicks, 0);
257
- const topNConcentration = totalClicks > 0 ? topNClicks / totalClicks : 0;
258
- let riskLevel = "low";
259
- if (hhi > 2500) riskLevel = "high";
260
- else if (hhi > 1500) riskLevel = "medium";
261
- return {
262
- giniCoefficient,
263
- hhi,
264
- topNConcentration,
265
- topNItems,
266
- totalItems: items.length,
267
- totalClicks,
268
- riskLevel
269
- };
270
- }
271
- /**
272
- * Convenience wrapper for page concentration analysis.
273
- */
274
- function analyzePageConcentration(pages, options) {
275
- return analyzeConcentration(pages.map((p) => ({
276
- key: p.page,
277
- clicks: num(p.clicks)
278
- })), options);
279
- }
280
- /**
281
- * Convenience wrapper for keyword concentration analysis.
282
- */
283
- function analyzeKeywordConcentration(keywords, options) {
284
- return analyzeConcentration(keywords.map((k) => ({
285
- key: k.keyword,
286
- clicks: num(k.clicks)
287
- })), options);
288
- }
289
-
290
- //#endregion
291
- //#region src/analysis/decay.ts
292
- const SORT_ORDER$1 = {
293
- lostClicks: "desc",
294
- declinePercent: "desc",
295
- currentClicks: "asc"
296
- };
297
- const sortResults$3 = createSorter((item, metric) => item[metric], "lostClicks");
298
- /**
299
- * Identifies "decaying" content - pages that have lost significant traffic.
300
- * Useful for finding old content that needs updating.
301
- *
302
- * @param input Current and previous period page data
303
- * @param options Filtering and sorting options
304
- */
305
- function analyzeDecay(input, options = {}) {
306
- const { minPreviousClicks = 50, threshold = .2, sortBy = "lostClicks" } = options;
307
- const currentMap = /* @__PURE__ */ new Map();
308
- for (const row of input.current) currentMap.set(row.page, {
309
- clicks: num(row.clicks),
310
- position: num(row.position)
311
- });
312
- const previousMap = /* @__PURE__ */ new Map();
313
- for (const row of input.previous) {
314
- const clicks = num(row.clicks);
315
- if (clicks >= minPreviousClicks) previousMap.set(row.page, {
316
- clicks,
317
- position: num(row.position)
318
- });
319
- }
320
- const results = [];
321
- for (const [page$1, prev] of previousMap) {
322
- const curr = currentMap.get(page$1) || {
323
- clicks: 0,
324
- position: 0
325
- };
326
- const lostClicks = prev.clicks - curr.clicks;
327
- const declinePercent = prev.clicks > 0 ? lostClicks / prev.clicks : 0;
328
- if (declinePercent >= threshold && lostClicks > 0) results.push({
329
- page: page$1,
330
- currentClicks: curr.clicks,
331
- previousClicks: prev.clicks,
332
- lostClicks,
333
- declinePercent,
334
- currentPosition: curr.position,
335
- previousPosition: prev.position,
336
- positionDrop: curr.position - prev.position
337
- });
338
- }
339
- return sortResults$3(results, sortBy, SORT_ORDER$1[sortBy]);
340
- }
341
-
342
- //#endregion
343
- //#region src/analysis/movers.ts
344
- function percentDifference$1(current, previous) {
345
- if (previous === 0) return current > 0 ? 100 : 0;
346
- return (current - previous) / previous * 100;
347
- }
348
- /**
349
- * Identifies movers and shakers - keywords with significant recent changes.
350
- * Compares recent period against baseline period.
351
- *
352
- * @param input Current and previous keyword data
353
- * @param options Filtering and sorting options
354
- */
355
- function analyzeMovers(input, options = {}) {
356
- const { changeThreshold = .2, minImpressions = 50, sortBy = "clicksChange" } = options;
357
- const normFactor = input.normalizationFactor ?? 1;
358
- const baselineMap = /* @__PURE__ */ new Map();
359
- for (const row of input.previous) baselineMap.set(row.keyword, {
360
- clicks: num(row.clicks) / normFactor,
361
- impressions: num(row.impressions) / normFactor,
362
- position: num(row.position),
363
- page: row.page ?? null
364
- });
365
- const pageMap = /* @__PURE__ */ new Map();
366
- for (const row of input.current) if (!pageMap.has(row.keyword) && row.page) pageMap.set(row.keyword, row.page);
367
- for (const row of input.previous) if (!pageMap.has(row.keyword) && row.page) pageMap.set(row.keyword, row.page);
368
- const rising = [];
369
- const declining = [];
370
- const stable = [];
371
- for (const row of input.current) {
372
- const impressions = num(row.impressions);
373
- const clicks = num(row.clicks);
374
- const position = num(row.position);
375
- if (impressions < minImpressions) continue;
376
- const baseline = baselineMap.get(row.keyword) || {
377
- clicks: 0,
378
- impressions: 0,
379
- position: 0,
380
- page: null
381
- };
382
- const clicksChangePercent = percentDifference$1(clicks, baseline.clicks);
383
- const impressionsChangePercent = percentDifference$1(impressions, baseline.impressions);
384
- const data = {
385
- keyword: row.keyword,
386
- page: pageMap.get(row.keyword) ?? null,
387
- recentClicks: clicks,
388
- recentImpressions: impressions,
389
- recentPosition: position,
390
- baselineClicks: Math.round(baseline.clicks),
391
- baselineImpressions: Math.round(baseline.impressions),
392
- baselinePosition: baseline.position,
393
- clicksChange: clicks - Math.round(baseline.clicks),
394
- clicksChangePercent,
395
- impressionsChangePercent,
396
- positionChange: position - baseline.position
397
- };
398
- const absChange = Math.abs(clicksChangePercent / 100);
399
- if (clicksChangePercent > 0 && absChange >= changeThreshold) rising.push(data);
400
- else if (clicksChangePercent < 0 && absChange >= changeThreshold) declining.push(data);
401
- else stable.push(data);
402
- }
403
- const sortFn = (a, b) => {
404
- switch (sortBy) {
405
- case "clicks": return b.recentClicks - a.recentClicks;
406
- case "impressions": return b.recentImpressions - a.recentImpressions;
407
- case "clicksChange": return Math.abs(b.clicksChangePercent) - Math.abs(a.clicksChangePercent);
408
- case "impressionsChange": return Math.abs(b.impressionsChangePercent) - Math.abs(a.impressionsChangePercent);
409
- case "positionChange": return Math.abs(b.positionChange) - Math.abs(a.positionChange);
410
- default: return Math.abs(b.clicksChangePercent) - Math.abs(a.clicksChangePercent);
411
- }
412
- };
413
- rising.sort(sortFn);
414
- declining.sort(sortFn);
415
- stable.sort((a, b) => b.recentClicks - a.recentClicks);
416
- return {
417
- rising,
418
- declining,
419
- stable
420
- };
421
- }
422
-
423
- //#endregion
424
- //#region src/analysis/opportunity.ts
425
- const EXPECTED_CTR_BY_POSITION = {
426
- 1: .3,
427
- 2: .15,
428
- 3: .1,
429
- 4: .07,
430
- 5: .05,
431
- 6: .04,
432
- 7: .03,
433
- 8: .025,
434
- 9: .02,
435
- 10: .015
436
- };
437
- function getExpectedCtr(position) {
438
- return EXPECTED_CTR_BY_POSITION[Math.round(Math.max(1, Math.min(position, 10)))] || .01;
439
- }
440
- /**
441
- * Position score: higher for positions 4-20 (improvable range).
442
- * Peak opportunity at positions 8-15.
443
- */
444
- function calculatePositionScore(position) {
445
- if (position <= 3) return .2;
446
- if (position > 50) return .1;
447
- const distance = Math.abs(position - 11);
448
- return Math.max(0, 1 - distance / 15);
449
- }
450
- /**
451
- * Impression score using log scale.
452
- * Higher impressions = more traffic potential.
453
- */
454
- function calculateImpressionScore(impressions) {
455
- if (impressions <= 0) return 0;
456
- return Math.min(Math.log10(impressions) / 5, 1);
457
- }
458
- /**
459
- * CTR gap score: difference between actual and expected CTR.
460
- * Higher gap = more opportunity for improvement.
461
- */
462
- function calculateCtrGapScore(actualCtr, position) {
463
- const expectedCtr = getExpectedCtr(position);
464
- if (actualCtr >= expectedCtr) return 0;
465
- const gap = expectedCtr - actualCtr;
466
- return Math.min(gap / expectedCtr, 1);
467
- }
468
- const SORT_ORDER = {
469
- opportunityScore: "desc",
470
- potentialClicks: "desc",
471
- impressions: "desc",
472
- position: "asc"
473
- };
474
- const sortResults$2 = createSorter((item, metric) => item[metric], "opportunityScore");
475
- /**
476
- * Scores keywords by optimization opportunity.
477
- * Composite score combining position, impressions, and CTR gap factors.
478
- */
479
- function analyzeOpportunity(keywords, options = {}) {
480
- const { minImpressions = 100, weights = {}, sortBy = "opportunityScore" } = options;
481
- const positionWeight = weights.position ?? 1;
482
- const impressionsWeight = weights.impressions ?? 1;
483
- const ctrGapWeight = weights.ctrGap ?? 1;
484
- const results = [];
485
- for (const row of keywords) {
486
- const impressions = num(row.impressions);
487
- const position = num(row.position);
488
- const ctr = num(row.ctr);
489
- const clicks = num(row.clicks);
490
- if (impressions < minImpressions) continue;
491
- const positionScore = calculatePositionScore(position);
492
- const impressionScore = calculateImpressionScore(impressions);
493
- const ctrGapScore = calculateCtrGapScore(ctr, position);
494
- const geometricMean = (positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight) ** (1 / (positionWeight + impressionsWeight + ctrGapWeight));
495
- const opportunityScore = Math.round(geometricMean * 100);
496
- const targetCtr = getExpectedCtr(Math.min(3, position));
497
- const potentialClicks = Math.round(impressions * targetCtr);
498
- results.push({
499
- keyword: row.keyword,
500
- page: row.page ?? null,
501
- clicks,
502
- impressions,
503
- ctr,
504
- position,
505
- opportunityScore,
506
- potentialClicks,
507
- factors: {
508
- positionScore,
509
- impressionScore,
510
- ctrGapScore
511
- }
512
- });
513
- }
514
- return sortResults$2(results, sortBy, SORT_ORDER[sortBy]);
515
- }
516
-
517
- //#endregion
518
- //#region src/analysis/seasonality.ts
519
- /**
520
- * Calculates coefficient of variation (CV) as a seasonality strength measure.
521
- * CV = std dev / mean, capped at 1 for the result.
522
- */
523
- function calculateCV(values) {
524
- if (values.length === 0) return 0;
525
- const mean = values.reduce((a, b) => a + b, 0) / values.length;
526
- if (mean === 0) return 0;
527
- const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
528
- const stdDev = Math.sqrt(variance);
529
- return Math.min(stdDev / mean, 1);
530
- }
531
- /**
532
- * Detects seasonality patterns by analyzing monthly traffic variation.
533
- * Identifies peaks (>1.5x average) and troughs (<0.5x average).
534
- *
535
- * @param dates Array of date metrics (each row is one day)
536
- * @param options Analysis options
537
- */
538
- function analyzeSeasonality(dates, options = {}) {
539
- const { metric = "clicks" } = options;
540
- if (dates.length === 0) return {
541
- hasSeasonality: false,
542
- strength: 0,
543
- peakMonths: [],
544
- troughMonths: [],
545
- monthlyBreakdown: [],
546
- insufficientData: true
547
- };
548
- const monthlyMap = /* @__PURE__ */ new Map();
549
- for (const row of dates) {
550
- const month = row.date.substring(0, 7);
551
- const value = metric === "clicks" ? row.clicks : row.impressions;
552
- monthlyMap.set(month, (monthlyMap.get(month) || 0) + value);
553
- }
554
- const months = Array.from(monthlyMap.keys()).sort();
555
- const values = months.map((m) => monthlyMap.get(m) || 0);
556
- const insufficientData = months.length < 12;
557
- const totalValue = values.reduce((a, b) => a + b, 0);
558
- const avgValue = values.length > 0 ? totalValue / values.length : 0;
559
- const monthlyBreakdown = months.map((month, i) => {
560
- const value = values[i];
561
- const vsAverage = avgValue > 0 ? value / avgValue : 0;
562
- return {
563
- month,
564
- value,
565
- vsAverage,
566
- isPeak: vsAverage > 1.5,
567
- isTrough: vsAverage < .5
568
- };
569
- });
570
- const peakMonths = [...new Set(monthlyBreakdown.filter((m) => m.isPeak).map((m) => m.month.substring(5, 7)))];
571
- const troughMonths = [...new Set(monthlyBreakdown.filter((m) => m.isTrough).map((m) => m.month.substring(5, 7)))];
572
- const strength = calculateCV(values);
573
- return {
574
- hasSeasonality: peakMonths.length > 0 || troughMonths.length > 0 || strength > .3,
575
- strength,
576
- peakMonths,
577
- troughMonths,
578
- monthlyBreakdown,
579
- insufficientData
580
- };
581
- }
582
-
583
- //#endregion
584
- //#region src/analysis/striking-distance.ts
585
- const sortResults$1 = createSorter((item, metric) => item[metric], "potentialClicks");
586
- /**
587
- * Finds striking distance keywords - high impressions, low CTR, position 4-20.
588
- * These are "quick wins" that could gain significant traffic with small ranking improvements.
589
- */
590
- function analyzeStrikingDistance(keywords, options = {}) {
591
- const { minPosition = 4, maxPosition = 20, minImpressions = 100, maxCtr = .05, sortBy = "potentialClicks", sortOrder = "desc" } = options;
592
- const results = [];
593
- for (const row of keywords) {
594
- const position = num(row.position);
595
- const impressions = num(row.impressions);
596
- const ctr = num(row.ctr);
597
- const clicks = num(row.clicks);
598
- if (position < minPosition || position > maxPosition) continue;
599
- if (impressions < minImpressions) continue;
600
- if (ctr > maxCtr) continue;
601
- const potentialClicks = Math.round(impressions * .15);
602
- results.push({
603
- keyword: row.keyword,
604
- page: row.page ?? null,
605
- clicks,
606
- impressions,
607
- ctr,
608
- position,
609
- potentialClicks
610
- });
611
- }
612
- return sortResults$1(results, sortBy, sortOrder);
613
- }
614
-
615
- //#endregion
616
- //#region src/analysis/zero-click.ts
617
- const sortResults = createSorter((item) => item.impressions, "impressions");
618
- /**
619
- * Identifies potential "Zero-Click" queries.
620
- * These are high-volume queries where you rank well but get few clicks,
621
- * often due to SERP features (Answer Boxes, Knowledge Panels, AI Overviews).
622
- *
623
- * @param rows Query+page data rows
624
- * @param options Filtering options
625
- */
626
- function analyzeZeroClick(rows, options = {}) {
627
- const { minImpressions = 1e3, maxCtr = .03, maxPosition = 10 } = options;
628
- const queryMap = /* @__PURE__ */ new Map();
629
- for (const row of rows) {
630
- if (row.impressions < minImpressions) continue;
631
- if (row.position > maxPosition) continue;
632
- if (row.ctr > maxCtr) continue;
633
- const existing = queryMap.get(row.query);
634
- if (!existing || row.position < existing.position) queryMap.set(row.query, {
635
- query: row.query,
636
- page: row.page,
637
- clicks: row.clicks,
638
- impressions: row.impressions,
639
- ctr: row.ctr,
640
- position: row.position
641
- });
642
- }
643
- return sortResults(Array.from(queryMap.values()), "impressions", "desc");
644
- }
645
-
646
- //#endregion
647
3
  //#region src/api/indexing.ts
648
4
  /**
649
5
  * Request Google to index or remove a URL via the Indexing API.
@@ -691,7 +47,7 @@ async function batchRequestIndexing(client, urls, options = {}) {
691
47
  * Inspects a URL in Google Search Console to check its indexing status.
692
48
  */
693
49
  async function inspectUrl(client, siteUrl, inspectionUrl) {
694
- const inspection = (await client.urlInspection.inspect(siteUrl, inspectionUrl)).inspectionResult;
50
+ const inspection = (await client.inspect(siteUrl, inspectionUrl)).inspectionResult;
695
51
  return {
696
52
  inspection,
697
53
  isIndexed: inspection?.indexStatusResult?.verdict === "PASS"
@@ -720,2444 +76,290 @@ async function batchInspectUrls(client, siteUrl, urls, options = {}) {
720
76
  }
721
77
 
722
78
  //#endregion
723
- //#region src/utils/dayjs.ts
724
- _dayjs.extend(utc);
725
- _dayjs.extend(timezone);
726
- function dayjs(date$1) {
727
- return _dayjs(date$1);
728
- }
729
- function currentPstDate() {
730
- return dayjs().tz("America/Los_Angeles").hour(12).minute(0).second(0).format("YYYY-MM-DD");
731
- }
732
- function dayjsPst() {
733
- return dayjs().tz("America/Los_Angeles").hour(12).minute(0).second(0);
734
- }
735
-
736
- //#endregion
737
- //#region src/utils/format.ts
738
- /**
739
- * Formats a date for GSC API queries (YYYY-MM-DD format).
740
- * @param d - Date object, date string, or null/undefined
741
- * @returns Formatted date string or null/undefined if input is falsy
742
- */
743
- function formatDateGsc(d) {
744
- if (!d) return d;
745
- if (typeof d === "string") return d;
746
- return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`;
747
- }
748
- /**
749
- * Calculates the percentage difference between two values.
750
- * Returns 0 if either value is null, undefined, or 0 (no meaningful comparison).
751
- * @param a - Current value
752
- * @param b - Previous/comparison value
753
- * @returns Percentage difference (positive = increase, negative = decrease), or 0 if either value is falsy
754
- */
755
- function percentDifference(a, b) {
756
- if (!b || !a) return 0;
757
- return (a - b) / ((a + b) / 2) * 100;
758
- }
759
-
760
- //#endregion
761
- //#region src/api/search-analytics/query.ts
762
- /**
763
- * Recursively queries GSC search analytics, automatically handling pagination for large datasets.
764
- */
765
- async function queryRecursive(client, siteUrl, query$1, options = {}) {
766
- const rowLimit = query$1.rowLimit || 25e3;
767
- const rows = [];
768
- let page$1 = 1;
769
- const initialRows = (await client.searchAnalytics.query(siteUrl, {
770
- ...query$1,
771
- startRow: 0,
772
- rowLimit
773
- })).rows || [];
774
- rows.push(...initialRows);
775
- options.onProgress?.(rows.length);
776
- if (initialRows.length === rowLimit) while (true) {
777
- page$1++;
778
- const nextRows = (await client.searchAnalytics.query(siteUrl, {
779
- ...query$1,
780
- startRow: rows.length,
781
- rowLimit
782
- })).rows || [];
783
- rows.push(...nextRows);
784
- options.onProgress?.(rows.length);
785
- if (nextRows.length < rowLimit) break;
786
- }
787
- return {
788
- rows,
789
- pages: page$1
790
- };
791
- }
792
- /**
793
- * Creates a GSC search analytics query request body with standard defaults.
794
- */
795
- function createQueryBody(options = {}) {
796
- const { domain, period = {
797
- start: dayjs().subtract(30, "day").toDate(),
798
- end: dayjs().toDate()
799
- }, filters = [], type = "web", dataState = "all", aggregationType = "byPage", rowLimit = 25e3 } = options;
800
- const allFilters = [...filters];
801
- if (aggregationType === "byPage") {
802
- allFilters.unshift({
803
- dimension: "page",
804
- operator: "excludingRegex",
805
- expression: `#`
806
- });
807
- if (domain) allFilters.unshift({
808
- dimension: "page",
809
- operator: "includingRegex",
810
- expression: `^${withoutTrailingSlash(domain).replace(/\./g, "\\.")}/.*`
811
- });
812
- }
813
- const result = {
814
- type,
815
- aggregationType,
816
- dataState,
817
- startDate: formatDateGsc(period.start),
818
- endDate: formatDateGsc(period.end),
819
- rowLimit
820
- };
821
- if (allFilters.length > 0) result.dimensionFilterGroups = [{ filters: allFilters }];
822
- return result;
823
- }
824
- /**
825
- * Helper to add search appearance dimension filter.
826
- * Use with spread: createQueryBody({ ...withSearchAppearance('AMP'), period })
827
- */
828
- function withSearchAppearance(appearance) {
829
- return { filters: [{
830
- dimension: "searchAppearance",
831
- operator: "equals",
832
- expression: appearance
833
- }] };
834
- }
835
- /**
836
- * Helper to set data type (web, image, video, news, discover, googleNews).
837
- * Use with spread: createQueryBody({ ...withDataType('image'), period })
838
- */
839
- function withDataType(type) {
840
- return { type };
841
- }
842
- /**
843
- * Helper to include fresh/unfinalized data (last 3 days).
844
- * Use with spread: createQueryBody({ ...withFreshData(), period })
845
- */
846
- function withFreshData() {
847
- return { dataState: "all" };
848
- }
849
- /**
850
- * Helper to use only finalized data (excludes last 3 days).
851
- * Use with spread: createQueryBody({ ...withFinalData(), period })
852
- */
853
- function withFinalData() {
854
- return { dataState: "final" };
855
- }
856
- /**
857
- * Helper to use byProperty aggregation (domain-level rollup).
858
- * Use for sc-domain: properties to get true totals.
859
- * Use with spread: createQueryBody({ ...withPropertyAggregation(), period })
860
- */
861
- function withPropertyAggregation() {
862
- return { aggregationType: "byProperty" };
863
- }
864
-
865
- //#endregion
866
- //#region src/api/search-analytics/analytics.ts
867
- /**
868
- * Fetches overall site analytics summary with period comparison and keyword data.
869
- */
870
- async function fetchAnalyticsWithComparison(client, siteUrl, options = {}) {
871
- const [currentSummary, previousSummary, currentKeywords, previousKeywords] = await Promise.all([
872
- client.searchAnalytics.query(siteUrl, { ...createQueryBody(options) }).then((res) => (res.rows || [])[0] || {}),
873
- options.prevPeriod ? client.searchAnalytics.query(siteUrl, { ...createQueryBody({
874
- ...options,
875
- period: options.prevPeriod
876
- }) }).then((res) => (res.rows || [])[0] || {}) : Promise.resolve({}),
877
- client.searchAnalytics.query(siteUrl, {
878
- ...createQueryBody(options),
879
- dimensions: ["date", "query"]
880
- }).then((res) => res.rows || []),
881
- options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
882
- ...createQueryBody({
883
- ...options,
884
- period: options.prevPeriod
885
- }),
886
- dimensions: ["date", "query"]
887
- }).then((res) => res.rows || []) : Promise.resolve([])
888
- ]);
889
- return {
890
- current: [{
891
- ...currentSummary,
892
- keywords: currentKeywords
893
- }],
894
- previous: [{
895
- ...previousSummary,
896
- keywords: previousKeywords
897
- }],
898
- metadata: {
899
- currentCount: 1,
900
- previousCount: options.prevPeriod ? 1 : 0,
901
- currentKeywordCount: currentKeywords.length,
902
- previousKeywordCount: previousKeywords.length
903
- }
904
- };
905
- }
906
-
907
- //#endregion
908
- //#region src/utils/countries.ts
909
- var countries_default = [
910
- {
911
- "name": "Afghanistan",
912
- "alpha-2": "AF",
913
- "alpha-3": "AFG",
914
- "country-code": "004"
915
- },
916
- {
917
- "name": "Åland Islands",
918
- "alpha-2": "AX",
919
- "alpha-3": "ALA",
920
- "country-code": "248"
921
- },
922
- {
923
- "name": "Albania",
924
- "alpha-2": "AL",
925
- "alpha-3": "ALB",
926
- "country-code": "008"
927
- },
928
- {
929
- "name": "Algeria",
930
- "alpha-2": "DZ",
931
- "alpha-3": "DZA",
932
- "country-code": "012"
933
- },
934
- {
935
- "name": "American Samoa",
936
- "alpha-2": "AS",
937
- "alpha-3": "ASM",
938
- "country-code": "016"
939
- },
940
- {
941
- "name": "Andorra",
942
- "alpha-2": "AD",
943
- "alpha-3": "AND",
944
- "country-code": "020"
945
- },
946
- {
947
- "name": "Angola",
948
- "alpha-2": "AO",
949
- "alpha-3": "AGO",
950
- "country-code": "024"
951
- },
952
- {
953
- "name": "Anguilla",
954
- "alpha-2": "AI",
955
- "alpha-3": "AIA",
956
- "country-code": "660"
957
- },
958
- {
959
- "name": "Antarctica",
960
- "alpha-2": "AQ",
961
- "alpha-3": "ATA",
962
- "country-code": "010"
963
- },
964
- {
965
- "name": "Antigua and Barbuda",
966
- "alpha-2": "AG",
967
- "alpha-3": "ATG",
968
- "country-code": "028"
969
- },
970
- {
971
- "name": "Argentina",
972
- "alpha-2": "AR",
973
- "alpha-3": "ARG",
974
- "country-code": "032"
975
- },
976
- {
977
- "name": "Armenia",
978
- "alpha-2": "AM",
979
- "alpha-3": "ARM",
980
- "country-code": "051"
981
- },
982
- {
983
- "name": "Aruba",
984
- "alpha-2": "AW",
985
- "alpha-3": "ABW",
986
- "country-code": "533"
987
- },
988
- {
989
- "name": "Australia",
990
- "alpha-2": "AU",
991
- "alpha-3": "AUS",
992
- "country-code": "036"
993
- },
994
- {
995
- "name": "Austria",
996
- "alpha-2": "AT",
997
- "alpha-3": "AUT",
998
- "country-code": "040"
999
- },
1000
- {
1001
- "name": "Azerbaijan",
1002
- "alpha-2": "AZ",
1003
- "alpha-3": "AZE",
1004
- "country-code": "031"
1005
- },
1006
- {
1007
- "name": "Bahamas",
1008
- "alpha-2": "BS",
1009
- "alpha-3": "BHS",
1010
- "country-code": "044"
1011
- },
1012
- {
1013
- "name": "Bahrain",
1014
- "alpha-2": "BH",
1015
- "alpha-3": "BHR",
1016
- "country-code": "048"
1017
- },
1018
- {
1019
- "name": "Bangladesh",
1020
- "alpha-2": "BD",
1021
- "alpha-3": "BGD",
1022
- "country-code": "050"
1023
- },
1024
- {
1025
- "name": "Barbados",
1026
- "alpha-2": "BB",
1027
- "alpha-3": "BRB",
1028
- "country-code": "052"
1029
- },
1030
- {
1031
- "name": "Belarus",
1032
- "alpha-2": "BY",
1033
- "alpha-3": "BLR",
1034
- "country-code": "112"
1035
- },
1036
- {
1037
- "name": "Belgium",
1038
- "alpha-2": "BE",
1039
- "alpha-3": "BEL",
1040
- "country-code": "056"
1041
- },
1042
- {
1043
- "name": "Belize",
1044
- "alpha-2": "BZ",
1045
- "alpha-3": "BLZ",
1046
- "country-code": "084"
1047
- },
1048
- {
1049
- "name": "Benin",
1050
- "alpha-2": "BJ",
1051
- "alpha-3": "BEN",
1052
- "country-code": "204"
1053
- },
1054
- {
1055
- "name": "Bermuda",
1056
- "alpha-2": "BM",
1057
- "alpha-3": "BMU",
1058
- "country-code": "060"
1059
- },
1060
- {
1061
- "name": "Bhutan",
1062
- "alpha-2": "BT",
1063
- "alpha-3": "BTN",
1064
- "country-code": "064"
1065
- },
1066
- {
1067
- "name": "Bolivia (Plurinational State of)",
1068
- "alpha-2": "BO",
1069
- "alpha-3": "BOL",
1070
- "country-code": "068"
1071
- },
1072
- {
1073
- "name": "Bonaire, Sint Eustatius and Saba",
1074
- "alpha-2": "BQ",
1075
- "alpha-3": "BES",
1076
- "country-code": "535"
1077
- },
1078
- {
1079
- "name": "Bosnia and Herzegovina",
1080
- "alpha-2": "BA",
1081
- "alpha-3": "BIH",
1082
- "country-code": "070"
1083
- },
1084
- {
1085
- "name": "Botswana",
1086
- "alpha-2": "BW",
1087
- "alpha-3": "BWA",
1088
- "country-code": "072"
1089
- },
1090
- {
1091
- "name": "Bouvet Island",
1092
- "alpha-2": "BV",
1093
- "alpha-3": "BVT",
1094
- "country-code": "074"
1095
- },
1096
- {
1097
- "name": "Brazil",
1098
- "alpha-2": "BR",
1099
- "alpha-3": "BRA",
1100
- "country-code": "076"
1101
- },
1102
- {
1103
- "name": "British Indian Ocean Territory",
1104
- "alpha-2": "IO",
1105
- "alpha-3": "IOT",
1106
- "country-code": "086"
1107
- },
1108
- {
1109
- "name": "Brunei Darussalam",
1110
- "alpha-2": "BN",
1111
- "alpha-3": "BRN",
1112
- "country-code": "096"
1113
- },
1114
- {
1115
- "name": "Bulgaria",
1116
- "alpha-2": "BG",
1117
- "alpha-3": "BGR",
1118
- "country-code": "100"
1119
- },
1120
- {
1121
- "name": "Burkina Faso",
1122
- "alpha-2": "BF",
1123
- "alpha-3": "BFA",
1124
- "country-code": "854"
1125
- },
1126
- {
1127
- "name": "Burundi",
1128
- "alpha-2": "BI",
1129
- "alpha-3": "BDI",
1130
- "country-code": "108"
1131
- },
1132
- {
1133
- "name": "Cabo Verde",
1134
- "alpha-2": "CV",
1135
- "alpha-3": "CPV",
1136
- "country-code": "132"
1137
- },
1138
- {
1139
- "name": "Cambodia",
1140
- "alpha-2": "KH",
1141
- "alpha-3": "KHM",
1142
- "country-code": "116"
1143
- },
1144
- {
1145
- "name": "Cameroon",
1146
- "alpha-2": "CM",
1147
- "alpha-3": "CMR",
1148
- "country-code": "120"
1149
- },
1150
- {
1151
- "name": "Canada",
1152
- "alpha-2": "CA",
1153
- "alpha-3": "CAN",
1154
- "country-code": "124"
1155
- },
1156
- {
1157
- "name": "Cayman Islands",
1158
- "alpha-2": "KY",
1159
- "alpha-3": "CYM",
1160
- "country-code": "136"
1161
- },
1162
- {
1163
- "name": "Central African Republic",
1164
- "alpha-2": "CF",
1165
- "alpha-3": "CAF",
1166
- "country-code": "140"
1167
- },
1168
- {
1169
- "name": "Chad",
1170
- "alpha-2": "TD",
1171
- "alpha-3": "TCD",
1172
- "country-code": "148"
1173
- },
1174
- {
1175
- "name": "Chile",
1176
- "alpha-2": "CL",
1177
- "alpha-3": "CHL",
1178
- "country-code": "152"
1179
- },
1180
- {
1181
- "name": "China",
1182
- "alpha-2": "CN",
1183
- "alpha-3": "CHN",
1184
- "country-code": "156"
1185
- },
1186
- {
1187
- "name": "Christmas Island",
1188
- "alpha-2": "CX",
1189
- "alpha-3": "CXR",
1190
- "country-code": "162"
1191
- },
1192
- {
1193
- "name": "Cocos (Keeling) Islands",
1194
- "alpha-2": "CC",
1195
- "alpha-3": "CCK",
1196
- "country-code": "166"
1197
- },
1198
- {
1199
- "name": "Colombia",
1200
- "alpha-2": "CO",
1201
- "alpha-3": "COL",
1202
- "country-code": "170"
1203
- },
1204
- {
1205
- "name": "Comoros",
1206
- "alpha-2": "KM",
1207
- "alpha-3": "COM",
1208
- "country-code": "174"
1209
- },
1210
- {
1211
- "name": "Congo",
1212
- "alpha-2": "CG",
1213
- "alpha-3": "COG",
1214
- "country-code": "178"
1215
- },
1216
- {
1217
- "name": "Congo, Democratic Republic of the",
1218
- "alpha-2": "CD",
1219
- "alpha-3": "COD",
1220
- "country-code": "180"
1221
- },
1222
- {
1223
- "name": "Cook Islands",
1224
- "alpha-2": "CK",
1225
- "alpha-3": "COK",
1226
- "country-code": "184"
1227
- },
1228
- {
1229
- "name": "Costa Rica",
1230
- "alpha-2": "CR",
1231
- "alpha-3": "CRI",
1232
- "country-code": "188"
1233
- },
1234
- {
1235
- "name": "Côte d'Ivoire",
1236
- "alpha-2": "CI",
1237
- "alpha-3": "CIV",
1238
- "country-code": "384"
1239
- },
1240
- {
1241
- "name": "Croatia",
1242
- "alpha-2": "HR",
1243
- "alpha-3": "HRV",
1244
- "country-code": "191"
1245
- },
1246
- {
1247
- "name": "Cuba",
1248
- "alpha-2": "CU",
1249
- "alpha-3": "CUB",
1250
- "country-code": "192"
1251
- },
1252
- {
1253
- "name": "Curaçao",
1254
- "alpha-2": "CW",
1255
- "alpha-3": "CUW",
1256
- "country-code": "531"
1257
- },
1258
- {
1259
- "name": "Cyprus",
1260
- "alpha-2": "CY",
1261
- "alpha-3": "CYP",
1262
- "country-code": "196"
1263
- },
1264
- {
1265
- "name": "Czechia",
1266
- "alpha-2": "CZ",
1267
- "alpha-3": "CZE",
1268
- "country-code": "203"
1269
- },
1270
- {
1271
- "name": "Denmark",
1272
- "alpha-2": "DK",
1273
- "alpha-3": "DNK",
1274
- "country-code": "208"
1275
- },
1276
- {
1277
- "name": "Djibouti",
1278
- "alpha-2": "DJ",
1279
- "alpha-3": "DJI",
1280
- "country-code": "262"
1281
- },
1282
- {
1283
- "name": "Dominica",
1284
- "alpha-2": "DM",
1285
- "alpha-3": "DMA",
1286
- "country-code": "212"
1287
- },
1288
- {
1289
- "name": "Dominican Republic",
1290
- "alpha-2": "DO",
1291
- "alpha-3": "DOM",
1292
- "country-code": "214"
1293
- },
1294
- {
1295
- "name": "Ecuador",
1296
- "alpha-2": "EC",
1297
- "alpha-3": "ECU",
1298
- "country-code": "218"
1299
- },
1300
- {
1301
- "name": "Egypt",
1302
- "alpha-2": "EG",
1303
- "alpha-3": "EGY",
1304
- "country-code": "818"
1305
- },
1306
- {
1307
- "name": "El Salvador",
1308
- "alpha-2": "SV",
1309
- "alpha-3": "SLV",
1310
- "country-code": "222"
1311
- },
1312
- {
1313
- "name": "Equatorial Guinea",
1314
- "alpha-2": "GQ",
1315
- "alpha-3": "GNQ",
1316
- "country-code": "226"
1317
- },
1318
- {
1319
- "name": "Eritrea",
1320
- "alpha-2": "ER",
1321
- "alpha-3": "ERI",
1322
- "country-code": "232"
1323
- },
1324
- {
1325
- "name": "Estonia",
1326
- "alpha-2": "EE",
1327
- "alpha-3": "EST",
1328
- "country-code": "233"
1329
- },
1330
- {
1331
- "name": "Eswatini",
1332
- "alpha-2": "SZ",
1333
- "alpha-3": "SWZ",
1334
- "country-code": "748"
1335
- },
1336
- {
1337
- "name": "Ethiopia",
1338
- "alpha-2": "ET",
1339
- "alpha-3": "ETH",
1340
- "country-code": "231"
1341
- },
1342
- {
1343
- "name": "Falkland Islands (Malvinas)",
1344
- "alpha-2": "FK",
1345
- "alpha-3": "FLK",
1346
- "country-code": "238"
1347
- },
1348
- {
1349
- "name": "Faroe Islands",
1350
- "alpha-2": "FO",
1351
- "alpha-3": "FRO",
1352
- "country-code": "234"
1353
- },
1354
- {
1355
- "name": "Fiji",
1356
- "alpha-2": "FJ",
1357
- "alpha-3": "FJI",
1358
- "country-code": "242"
1359
- },
1360
- {
1361
- "name": "Finland",
1362
- "alpha-2": "FI",
1363
- "alpha-3": "FIN",
1364
- "country-code": "246"
1365
- },
1366
- {
1367
- "name": "France",
1368
- "alpha-2": "FR",
1369
- "alpha-3": "FRA",
1370
- "country-code": "250"
1371
- },
1372
- {
1373
- "name": "French Guiana",
1374
- "alpha-2": "GF",
1375
- "alpha-3": "GUF",
1376
- "country-code": "254"
1377
- },
1378
- {
1379
- "name": "French Polynesia",
1380
- "alpha-2": "PF",
1381
- "alpha-3": "PYF",
1382
- "country-code": "258"
1383
- },
1384
- {
1385
- "name": "French Southern Territories",
1386
- "alpha-2": "TF",
1387
- "alpha-3": "ATF",
1388
- "country-code": "260"
1389
- },
1390
- {
1391
- "name": "Gabon",
1392
- "alpha-2": "GA",
1393
- "alpha-3": "GAB",
1394
- "country-code": "266"
1395
- },
1396
- {
1397
- "name": "Gambia",
1398
- "alpha-2": "GM",
1399
- "alpha-3": "GMB",
1400
- "country-code": "270"
1401
- },
1402
- {
1403
- "name": "Georgia",
1404
- "alpha-2": "GE",
1405
- "alpha-3": "GEO",
1406
- "country-code": "268"
1407
- },
1408
- {
1409
- "name": "Germany",
1410
- "alpha-2": "DE",
1411
- "alpha-3": "DEU",
1412
- "country-code": "276"
1413
- },
1414
- {
1415
- "name": "Ghana",
1416
- "alpha-2": "GH",
1417
- "alpha-3": "GHA",
1418
- "country-code": "288"
1419
- },
1420
- {
1421
- "name": "Gibraltar",
1422
- "alpha-2": "GI",
1423
- "alpha-3": "GIB",
1424
- "country-code": "292"
1425
- },
1426
- {
1427
- "name": "Greece",
1428
- "alpha-2": "GR",
1429
- "alpha-3": "GRC",
1430
- "country-code": "300"
1431
- },
1432
- {
1433
- "name": "Greenland",
1434
- "alpha-2": "GL",
1435
- "alpha-3": "GRL",
1436
- "country-code": "304"
1437
- },
1438
- {
1439
- "name": "Grenada",
1440
- "alpha-2": "GD",
1441
- "alpha-3": "GRD",
1442
- "country-code": "308"
1443
- },
1444
- {
1445
- "name": "Guadeloupe",
1446
- "alpha-2": "GP",
1447
- "alpha-3": "GLP",
1448
- "country-code": "312"
1449
- },
1450
- {
1451
- "name": "Guam",
1452
- "alpha-2": "GU",
1453
- "alpha-3": "GUM",
1454
- "country-code": "316"
1455
- },
1456
- {
1457
- "name": "Guatemala",
1458
- "alpha-2": "GT",
1459
- "alpha-3": "GTM",
1460
- "country-code": "320"
1461
- },
1462
- {
1463
- "name": "Guernsey",
1464
- "alpha-2": "GG",
1465
- "alpha-3": "GGY",
1466
- "country-code": "831"
1467
- },
1468
- {
1469
- "name": "Guinea",
1470
- "alpha-2": "GN",
1471
- "alpha-3": "GIN",
1472
- "country-code": "324"
1473
- },
1474
- {
1475
- "name": "Guinea-Bissau",
1476
- "alpha-2": "GW",
1477
- "alpha-3": "GNB",
1478
- "country-code": "624"
1479
- },
1480
- {
1481
- "name": "Guyana",
1482
- "alpha-2": "GY",
1483
- "alpha-3": "GUY",
1484
- "country-code": "328"
1485
- },
1486
- {
1487
- "name": "Haiti",
1488
- "alpha-2": "HT",
1489
- "alpha-3": "HTI",
1490
- "country-code": "332"
1491
- },
1492
- {
1493
- "name": "Heard Island and McDonald Islands",
1494
- "alpha-2": "HM",
1495
- "alpha-3": "HMD",
1496
- "country-code": "334"
1497
- },
1498
- {
1499
- "name": "Holy See",
1500
- "alpha-2": "VA",
1501
- "alpha-3": "VAT",
1502
- "country-code": "336"
1503
- },
1504
- {
1505
- "name": "Honduras",
1506
- "alpha-2": "HN",
1507
- "alpha-3": "HND",
1508
- "country-code": "340"
1509
- },
1510
- {
1511
- "name": "Hong Kong",
1512
- "alpha-2": "HK",
1513
- "alpha-3": "HKG",
1514
- "country-code": "344"
1515
- },
1516
- {
1517
- "name": "Hungary",
1518
- "alpha-2": "HU",
1519
- "alpha-3": "HUN",
1520
- "country-code": "348"
1521
- },
1522
- {
1523
- "name": "Iceland",
1524
- "alpha-2": "IS",
1525
- "alpha-3": "ISL",
1526
- "country-code": "352"
1527
- },
1528
- {
1529
- "name": "India",
1530
- "alpha-2": "IN",
1531
- "alpha-3": "IND",
1532
- "country-code": "356"
1533
- },
1534
- {
1535
- "name": "Indonesia",
1536
- "alpha-2": "ID",
1537
- "alpha-3": "IDN",
1538
- "country-code": "360"
1539
- },
1540
- {
1541
- "name": "Iran (Islamic Republic of)",
1542
- "alpha-2": "IR",
1543
- "alpha-3": "IRN",
1544
- "country-code": "364"
1545
- },
1546
- {
1547
- "name": "Iraq",
1548
- "alpha-2": "IQ",
1549
- "alpha-3": "IRQ",
1550
- "country-code": "368"
1551
- },
1552
- {
1553
- "name": "Ireland",
1554
- "alpha-2": "IE",
1555
- "alpha-3": "IRL",
1556
- "country-code": "372"
1557
- },
1558
- {
1559
- "name": "Isle of Man",
1560
- "alpha-2": "IM",
1561
- "alpha-3": "IMN",
1562
- "country-code": "833"
1563
- },
1564
- {
1565
- "name": "Israel",
1566
- "alpha-2": "IL",
1567
- "alpha-3": "ISR",
1568
- "country-code": "376"
1569
- },
1570
- {
1571
- "name": "Italy",
1572
- "alpha-2": "IT",
1573
- "alpha-3": "ITA",
1574
- "country-code": "380"
1575
- },
1576
- {
1577
- "name": "Jamaica",
1578
- "alpha-2": "JM",
1579
- "alpha-3": "JAM",
1580
- "country-code": "388"
1581
- },
1582
- {
1583
- "name": "Japan",
1584
- "alpha-2": "JP",
1585
- "alpha-3": "JPN",
1586
- "country-code": "392"
1587
- },
1588
- {
1589
- "name": "Jersey",
1590
- "alpha-2": "JE",
1591
- "alpha-3": "JEY",
1592
- "country-code": "832"
1593
- },
1594
- {
1595
- "name": "Jordan",
1596
- "alpha-2": "JO",
1597
- "alpha-3": "JOR",
1598
- "country-code": "400"
1599
- },
1600
- {
1601
- "name": "Kazakhstan",
1602
- "alpha-2": "KZ",
1603
- "alpha-3": "KAZ",
1604
- "country-code": "398"
1605
- },
1606
- {
1607
- "name": "Kenya",
1608
- "alpha-2": "KE",
1609
- "alpha-3": "KEN",
1610
- "country-code": "404"
1611
- },
1612
- {
1613
- "name": "Kiribati",
1614
- "alpha-2": "KI",
1615
- "alpha-3": "KIR",
1616
- "country-code": "296"
1617
- },
1618
- {
1619
- "name": "Korea (Democratic People's Republic of)",
1620
- "alpha-2": "KP",
1621
- "alpha-3": "PRK",
1622
- "country-code": "408"
1623
- },
1624
- {
1625
- "name": "Korea, Republic of",
1626
- "alpha-2": "KR",
1627
- "alpha-3": "KOR",
1628
- "country-code": "410"
1629
- },
1630
- {
1631
- "name": "Kuwait",
1632
- "alpha-2": "KW",
1633
- "alpha-3": "KWT",
1634
- "country-code": "414"
1635
- },
1636
- {
1637
- "name": "Kyrgyzstan",
1638
- "alpha-2": "KG",
1639
- "alpha-3": "KGZ",
1640
- "country-code": "417"
1641
- },
1642
- {
1643
- "name": "Lao People's Democratic Republic",
1644
- "alpha-2": "LA",
1645
- "alpha-3": "LAO",
1646
- "country-code": "418"
1647
- },
1648
- {
1649
- "name": "Latvia",
1650
- "alpha-2": "LV",
1651
- "alpha-3": "LVA",
1652
- "country-code": "428"
1653
- },
1654
- {
1655
- "name": "Lebanon",
1656
- "alpha-2": "LB",
1657
- "alpha-3": "LBN",
1658
- "country-code": "422"
1659
- },
1660
- {
1661
- "name": "Lesotho",
1662
- "alpha-2": "LS",
1663
- "alpha-3": "LSO",
1664
- "country-code": "426"
1665
- },
1666
- {
1667
- "name": "Liberia",
1668
- "alpha-2": "LR",
1669
- "alpha-3": "LBR",
1670
- "country-code": "430"
1671
- },
1672
- {
1673
- "name": "Libya",
1674
- "alpha-2": "LY",
1675
- "alpha-3": "LBY",
1676
- "country-code": "434"
1677
- },
1678
- {
1679
- "name": "Liechtenstein",
1680
- "alpha-2": "LI",
1681
- "alpha-3": "LIE",
1682
- "country-code": "438"
1683
- },
1684
- {
1685
- "name": "Lithuania",
1686
- "alpha-2": "LT",
1687
- "alpha-3": "LTU",
1688
- "country-code": "440"
1689
- },
1690
- {
1691
- "name": "Luxembourg",
1692
- "alpha-2": "LU",
1693
- "alpha-3": "LUX",
1694
- "country-code": "442"
1695
- },
1696
- {
1697
- "name": "Macao",
1698
- "alpha-2": "MO",
1699
- "alpha-3": "MAC",
1700
- "country-code": "446"
1701
- },
1702
- {
1703
- "name": "Madagascar",
1704
- "alpha-2": "MG",
1705
- "alpha-3": "MDG",
1706
- "country-code": "450"
1707
- },
1708
- {
1709
- "name": "Malawi",
1710
- "alpha-2": "MW",
1711
- "alpha-3": "MWI",
1712
- "country-code": "454"
1713
- },
1714
- {
1715
- "name": "Malaysia",
1716
- "alpha-2": "MY",
1717
- "alpha-3": "MYS",
1718
- "country-code": "458"
1719
- },
1720
- {
1721
- "name": "Maldives",
1722
- "alpha-2": "MV",
1723
- "alpha-3": "MDV",
1724
- "country-code": "462"
1725
- },
1726
- {
1727
- "name": "Mali",
1728
- "alpha-2": "ML",
1729
- "alpha-3": "MLI",
1730
- "country-code": "466"
1731
- },
1732
- {
1733
- "name": "Malta",
1734
- "alpha-2": "MT",
1735
- "alpha-3": "MLT",
1736
- "country-code": "470"
1737
- },
1738
- {
1739
- "name": "Marshall Islands",
1740
- "alpha-2": "MH",
1741
- "alpha-3": "MHL",
1742
- "country-code": "584"
1743
- },
1744
- {
1745
- "name": "Martinique",
1746
- "alpha-2": "MQ",
1747
- "alpha-3": "MTQ",
1748
- "country-code": "474"
1749
- },
1750
- {
1751
- "name": "Mauritania",
1752
- "alpha-2": "MR",
1753
- "alpha-3": "MRT",
1754
- "country-code": "478"
1755
- },
1756
- {
1757
- "name": "Mauritius",
1758
- "alpha-2": "MU",
1759
- "alpha-3": "MUS",
1760
- "country-code": "480"
1761
- },
1762
- {
1763
- "name": "Mayotte",
1764
- "alpha-2": "YT",
1765
- "alpha-3": "MYT",
1766
- "country-code": "175"
1767
- },
1768
- {
1769
- "name": "Mexico",
1770
- "alpha-2": "MX",
1771
- "alpha-3": "MEX",
1772
- "country-code": "484"
1773
- },
1774
- {
1775
- "name": "Micronesia (Federated States of)",
1776
- "alpha-2": "FM",
1777
- "alpha-3": "FSM",
1778
- "country-code": "583"
1779
- },
1780
- {
1781
- "name": "Moldova, Republic of",
1782
- "alpha-2": "MD",
1783
- "alpha-3": "MDA",
1784
- "country-code": "498"
1785
- },
1786
- {
1787
- "name": "Monaco",
1788
- "alpha-2": "MC",
1789
- "alpha-3": "MCO",
1790
- "country-code": "492"
1791
- },
1792
- {
1793
- "name": "Mongolia",
1794
- "alpha-2": "MN",
1795
- "alpha-3": "MNG",
1796
- "country-code": "496"
1797
- },
1798
- {
1799
- "name": "Montenegro",
1800
- "alpha-2": "ME",
1801
- "alpha-3": "MNE",
1802
- "country-code": "499"
1803
- },
1804
- {
1805
- "name": "Montserrat",
1806
- "alpha-2": "MS",
1807
- "alpha-3": "MSR",
1808
- "country-code": "500"
1809
- },
1810
- {
1811
- "name": "Morocco",
1812
- "alpha-2": "MA",
1813
- "alpha-3": "MAR",
1814
- "country-code": "504"
1815
- },
1816
- {
1817
- "name": "Mozambique",
1818
- "alpha-2": "MZ",
1819
- "alpha-3": "MOZ",
1820
- "country-code": "508"
1821
- },
1822
- {
1823
- "name": "Myanmar",
1824
- "alpha-2": "MM",
1825
- "alpha-3": "MMR",
1826
- "country-code": "104"
1827
- },
1828
- {
1829
- "name": "Namibia",
1830
- "alpha-2": "NA",
1831
- "alpha-3": "NAM",
1832
- "country-code": "516"
1833
- },
1834
- {
1835
- "name": "Nauru",
1836
- "alpha-2": "NR",
1837
- "alpha-3": "NRU",
1838
- "country-code": "520"
1839
- },
1840
- {
1841
- "name": "Nepal",
1842
- "alpha-2": "NP",
1843
- "alpha-3": "NPL",
1844
- "country-code": "524"
1845
- },
1846
- {
1847
- "name": "Netherlands",
1848
- "alpha-2": "NL",
1849
- "alpha-3": "NLD",
1850
- "country-code": "528"
1851
- },
1852
- {
1853
- "name": "New Caledonia",
1854
- "alpha-2": "NC",
1855
- "alpha-3": "NCL",
1856
- "country-code": "540"
1857
- },
1858
- {
1859
- "name": "New Zealand",
1860
- "alpha-2": "NZ",
1861
- "alpha-3": "NZL",
1862
- "country-code": "554"
1863
- },
1864
- {
1865
- "name": "Nicaragua",
1866
- "alpha-2": "NI",
1867
- "alpha-3": "NIC",
1868
- "country-code": "558"
1869
- },
1870
- {
1871
- "name": "Niger",
1872
- "alpha-2": "NE",
1873
- "alpha-3": "NER",
1874
- "country-code": "562"
1875
- },
1876
- {
1877
- "name": "Nigeria",
1878
- "alpha-2": "NG",
1879
- "alpha-3": "NGA",
1880
- "country-code": "566"
1881
- },
1882
- {
1883
- "name": "Niue",
1884
- "alpha-2": "NU",
1885
- "alpha-3": "NIU",
1886
- "country-code": "570"
1887
- },
1888
- {
1889
- "name": "Norfolk Island",
1890
- "alpha-2": "NF",
1891
- "alpha-3": "NFK",
1892
- "country-code": "574"
1893
- },
1894
- {
1895
- "name": "North Macedonia",
1896
- "alpha-2": "MK",
1897
- "alpha-3": "MKD",
1898
- "country-code": "807"
1899
- },
1900
- {
1901
- "name": "Northern Mariana Islands",
1902
- "alpha-2": "MP",
1903
- "alpha-3": "MNP",
1904
- "country-code": "580"
1905
- },
1906
- {
1907
- "name": "Norway",
1908
- "alpha-2": "NO",
1909
- "alpha-3": "NOR",
1910
- "country-code": "578"
1911
- },
1912
- {
1913
- "name": "Oman",
1914
- "alpha-2": "OM",
1915
- "alpha-3": "OMN",
1916
- "country-code": "512"
1917
- },
1918
- {
1919
- "name": "Pakistan",
1920
- "alpha-2": "PK",
1921
- "alpha-3": "PAK",
1922
- "country-code": "586"
1923
- },
1924
- {
1925
- "name": "Palau",
1926
- "alpha-2": "PW",
1927
- "alpha-3": "PLW",
1928
- "country-code": "585"
1929
- },
1930
- {
1931
- "name": "Palestine, State of",
1932
- "alpha-2": "PS",
1933
- "alpha-3": "PSE",
1934
- "country-code": "275"
1935
- },
1936
- {
1937
- "name": "Panama",
1938
- "alpha-2": "PA",
1939
- "alpha-3": "PAN",
1940
- "country-code": "591"
1941
- },
1942
- {
1943
- "name": "Papua New Guinea",
1944
- "alpha-2": "PG",
1945
- "alpha-3": "PNG",
1946
- "country-code": "598"
1947
- },
1948
- {
1949
- "name": "Paraguay",
1950
- "alpha-2": "PY",
1951
- "alpha-3": "PRY",
1952
- "country-code": "600"
1953
- },
1954
- {
1955
- "name": "Peru",
1956
- "alpha-2": "PE",
1957
- "alpha-3": "PER",
1958
- "country-code": "604"
1959
- },
1960
- {
1961
- "name": "Philippines",
1962
- "alpha-2": "PH",
1963
- "alpha-3": "PHL",
1964
- "country-code": "608"
1965
- },
1966
- {
1967
- "name": "Pitcairn",
1968
- "alpha-2": "PN",
1969
- "alpha-3": "PCN",
1970
- "country-code": "612"
1971
- },
1972
- {
1973
- "name": "Poland",
1974
- "alpha-2": "PL",
1975
- "alpha-3": "POL",
1976
- "country-code": "616"
1977
- },
1978
- {
1979
- "name": "Portugal",
1980
- "alpha-2": "PT",
1981
- "alpha-3": "PRT",
1982
- "country-code": "620"
1983
- },
1984
- {
1985
- "name": "Puerto Rico",
1986
- "alpha-2": "PR",
1987
- "alpha-3": "PRI",
1988
- "country-code": "630"
1989
- },
1990
- {
1991
- "name": "Qatar",
1992
- "alpha-2": "QA",
1993
- "alpha-3": "QAT",
1994
- "country-code": "634"
1995
- },
1996
- {
1997
- "name": "Réunion",
1998
- "alpha-2": "RE",
1999
- "alpha-3": "REU",
2000
- "country-code": "638"
2001
- },
2002
- {
2003
- "name": "Romania",
2004
- "alpha-2": "RO",
2005
- "alpha-3": "ROU",
2006
- "country-code": "642"
2007
- },
2008
- {
2009
- "name": "Russian Federation",
2010
- "alpha-2": "RU",
2011
- "alpha-3": "RUS",
2012
- "country-code": "643"
2013
- },
2014
- {
2015
- "name": "Rwanda",
2016
- "alpha-2": "RW",
2017
- "alpha-3": "RWA",
2018
- "country-code": "646"
2019
- },
2020
- {
2021
- "name": "Saint Barthélemy",
2022
- "alpha-2": "BL",
2023
- "alpha-3": "BLM",
2024
- "country-code": "652"
2025
- },
2026
- {
2027
- "name": "Saint Helena, Ascension and Tristan da Cunha",
2028
- "alpha-2": "SH",
2029
- "alpha-3": "SHN",
2030
- "country-code": "654"
2031
- },
2032
- {
2033
- "name": "Saint Kitts and Nevis",
2034
- "alpha-2": "KN",
2035
- "alpha-3": "KNA",
2036
- "country-code": "659"
2037
- },
2038
- {
2039
- "name": "Saint Lucia",
2040
- "alpha-2": "LC",
2041
- "alpha-3": "LCA",
2042
- "country-code": "662"
2043
- },
2044
- {
2045
- "name": "Saint Martin (French part)",
2046
- "alpha-2": "MF",
2047
- "alpha-3": "MAF",
2048
- "country-code": "663"
2049
- },
2050
- {
2051
- "name": "Saint Pierre and Miquelon",
2052
- "alpha-2": "PM",
2053
- "alpha-3": "SPM",
2054
- "country-code": "666"
2055
- },
2056
- {
2057
- "name": "Saint Vincent and the Grenadines",
2058
- "alpha-2": "VC",
2059
- "alpha-3": "VCT",
2060
- "country-code": "670"
2061
- },
2062
- {
2063
- "name": "Samoa",
2064
- "alpha-2": "WS",
2065
- "alpha-3": "WSM",
2066
- "country-code": "882"
2067
- },
2068
- {
2069
- "name": "San Marino",
2070
- "alpha-2": "SM",
2071
- "alpha-3": "SMR",
2072
- "country-code": "674"
2073
- },
2074
- {
2075
- "name": "Sao Tome and Principe",
2076
- "alpha-2": "ST",
2077
- "alpha-3": "STP",
2078
- "country-code": "678"
2079
- },
2080
- {
2081
- "name": "Saudi Arabia",
2082
- "alpha-2": "SA",
2083
- "alpha-3": "SAU",
2084
- "country-code": "682"
2085
- },
2086
- {
2087
- "name": "Senegal",
2088
- "alpha-2": "SN",
2089
- "alpha-3": "SEN",
2090
- "country-code": "686"
2091
- },
2092
- {
2093
- "name": "Serbia",
2094
- "alpha-2": "RS",
2095
- "alpha-3": "SRB",
2096
- "country-code": "688"
2097
- },
2098
- {
2099
- "name": "Seychelles",
2100
- "alpha-2": "SC",
2101
- "alpha-3": "SYC",
2102
- "country-code": "690"
2103
- },
2104
- {
2105
- "name": "Sierra Leone",
2106
- "alpha-2": "SL",
2107
- "alpha-3": "SLE",
2108
- "country-code": "694"
2109
- },
2110
- {
2111
- "name": "Singapore",
2112
- "alpha-2": "SG",
2113
- "alpha-3": "SGP",
2114
- "country-code": "702"
2115
- },
2116
- {
2117
- "name": "Sint Maarten (Dutch part)",
2118
- "alpha-2": "SX",
2119
- "alpha-3": "SXM",
2120
- "country-code": "534"
2121
- },
2122
- {
2123
- "name": "Slovakia",
2124
- "alpha-2": "SK",
2125
- "alpha-3": "SVK",
2126
- "country-code": "703"
2127
- },
2128
- {
2129
- "name": "Slovenia",
2130
- "alpha-2": "SI",
2131
- "alpha-3": "SVN",
2132
- "country-code": "705"
2133
- },
2134
- {
2135
- "name": "Solomon Islands",
2136
- "alpha-2": "SB",
2137
- "alpha-3": "SLB",
2138
- "country-code": "090"
2139
- },
2140
- {
2141
- "name": "Somalia",
2142
- "alpha-2": "SO",
2143
- "alpha-3": "SOM",
2144
- "country-code": "706"
2145
- },
2146
- {
2147
- "name": "South Africa",
2148
- "alpha-2": "ZA",
2149
- "alpha-3": "ZAF",
2150
- "country-code": "710"
2151
- },
2152
- {
2153
- "name": "South Georgia and the South Sandwich Islands",
2154
- "alpha-2": "GS",
2155
- "alpha-3": "SGS",
2156
- "country-code": "239"
2157
- },
2158
- {
2159
- "name": "South Sudan",
2160
- "alpha-2": "SS",
2161
- "alpha-3": "SSD",
2162
- "country-code": "728"
2163
- },
2164
- {
2165
- "name": "Spain",
2166
- "alpha-2": "ES",
2167
- "alpha-3": "ESP",
2168
- "country-code": "724"
2169
- },
2170
- {
2171
- "name": "Sri Lanka",
2172
- "alpha-2": "LK",
2173
- "alpha-3": "LKA",
2174
- "country-code": "144"
2175
- },
2176
- {
2177
- "name": "Sudan",
2178
- "alpha-2": "SD",
2179
- "alpha-3": "SDN",
2180
- "country-code": "729"
2181
- },
2182
- {
2183
- "name": "Suriname",
2184
- "alpha-2": "SR",
2185
- "alpha-3": "SUR",
2186
- "country-code": "740"
2187
- },
2188
- {
2189
- "name": "Svalbard and Jan Mayen",
2190
- "alpha-2": "SJ",
2191
- "alpha-3": "SJM",
2192
- "country-code": "744"
2193
- },
2194
- {
2195
- "name": "Sweden",
2196
- "alpha-2": "SE",
2197
- "alpha-3": "SWE",
2198
- "country-code": "752"
2199
- },
2200
- {
2201
- "name": "Switzerland",
2202
- "alpha-2": "CH",
2203
- "alpha-3": "CHE",
2204
- "country-code": "756"
2205
- },
2206
- {
2207
- "name": "Syrian Arab Republic",
2208
- "alpha-2": "SY",
2209
- "alpha-3": "SYR",
2210
- "country-code": "760"
2211
- },
2212
- {
2213
- "name": "Taiwan",
2214
- "alpha-2": "TW",
2215
- "alpha-3": "TWN",
2216
- "country-code": "158"
2217
- },
2218
- {
2219
- "name": "Tajikistan",
2220
- "alpha-2": "TJ",
2221
- "alpha-3": "TJK",
2222
- "country-code": "762"
2223
- },
2224
- {
2225
- "name": "Tanzania, United Republic of",
2226
- "alpha-2": "TZ",
2227
- "alpha-3": "TZA",
2228
- "country-code": "834"
2229
- },
2230
- {
2231
- "name": "Thailand",
2232
- "alpha-2": "TH",
2233
- "alpha-3": "THA",
2234
- "country-code": "764"
2235
- },
2236
- {
2237
- "name": "Timor-Leste",
2238
- "alpha-2": "TL",
2239
- "alpha-3": "TLS",
2240
- "country-code": "626"
2241
- },
2242
- {
2243
- "name": "Togo",
2244
- "alpha-2": "TG",
2245
- "alpha-3": "TGO",
2246
- "country-code": "768"
2247
- },
2248
- {
2249
- "name": "Tokelau",
2250
- "alpha-2": "TK",
2251
- "alpha-3": "TKL",
2252
- "country-code": "772"
2253
- },
2254
- {
2255
- "name": "Tonga",
2256
- "alpha-2": "TO",
2257
- "alpha-3": "TON",
2258
- "country-code": "776"
2259
- },
2260
- {
2261
- "name": "Trinidad and Tobago",
2262
- "alpha-2": "TT",
2263
- "alpha-3": "TTO",
2264
- "country-code": "780"
2265
- },
2266
- {
2267
- "name": "Tunisia",
2268
- "alpha-2": "TN",
2269
- "alpha-3": "TUN",
2270
- "country-code": "788"
2271
- },
2272
- {
2273
- "name": "Turkey",
2274
- "alpha-2": "TR",
2275
- "alpha-3": "TUR",
2276
- "country-code": "792"
2277
- },
2278
- {
2279
- "name": "Turkmenistan",
2280
- "alpha-2": "TM",
2281
- "alpha-3": "TKM",
2282
- "country-code": "795"
2283
- },
2284
- {
2285
- "name": "Turks and Caicos Islands",
2286
- "alpha-2": "TC",
2287
- "alpha-3": "TCA",
2288
- "country-code": "796"
2289
- },
2290
- {
2291
- "name": "Tuvalu",
2292
- "alpha-2": "TV",
2293
- "alpha-3": "TUV",
2294
- "country-code": "798"
2295
- },
2296
- {
2297
- "name": "Uganda",
2298
- "alpha-2": "UG",
2299
- "alpha-3": "UGA",
2300
- "country-code": "800"
2301
- },
2302
- {
2303
- "name": "Ukraine",
2304
- "alpha-2": "UA",
2305
- "alpha-3": "UKR",
2306
- "country-code": "804"
2307
- },
2308
- {
2309
- "name": "United Arab Emirates",
2310
- "alpha-2": "AE",
2311
- "alpha-3": "ARE",
2312
- "country-code": "784"
2313
- },
2314
- {
2315
- "name": "United Kingdom",
2316
- "alpha-2": "GB",
2317
- "alpha-3": "GBR",
2318
- "country-code": "826"
2319
- },
2320
- {
2321
- "name": "America",
2322
- "alpha-2": "US",
2323
- "alpha-3": "USA",
2324
- "country-code": "840"
2325
- },
2326
- {
2327
- "name": "United States Minor Outlying Islands",
2328
- "alpha-2": "UM",
2329
- "alpha-3": "UMI",
2330
- "country-code": "581"
2331
- },
2332
- {
2333
- "name": "Uruguay",
2334
- "alpha-2": "UY",
2335
- "alpha-3": "URY",
2336
- "country-code": "858"
2337
- },
2338
- {
2339
- "name": "Uzbekistan",
2340
- "alpha-2": "UZ",
2341
- "alpha-3": "UZB",
2342
- "country-code": "860"
2343
- },
2344
- {
2345
- "name": "Vanuatu",
2346
- "alpha-2": "VU",
2347
- "alpha-3": "VUT",
2348
- "country-code": "548"
2349
- },
2350
- {
2351
- "name": "Venezuela (Bolivarian Republic of)",
2352
- "alpha-2": "VE",
2353
- "alpha-3": "VEN",
2354
- "country-code": "862"
2355
- },
2356
- {
2357
- "name": "Vietnam",
2358
- "alpha-2": "VN",
2359
- "alpha-3": "VNM",
2360
- "country-code": "704"
2361
- },
2362
- {
2363
- "name": "Virgin Islands (British)",
2364
- "alpha-2": "VG",
2365
- "alpha-3": "VGB",
2366
- "country-code": "092"
2367
- },
2368
- {
2369
- "name": "Virgin Islands (U.S.)",
2370
- "alpha-2": "VI",
2371
- "alpha-3": "VIR",
2372
- "country-code": "850"
2373
- },
2374
- {
2375
- "name": "Wallis and Futuna",
2376
- "alpha-2": "WF",
2377
- "alpha-3": "WLF",
2378
- "country-code": "876"
2379
- },
2380
- {
2381
- "name": "Western Sahara",
2382
- "alpha-2": "EH",
2383
- "alpha-3": "ESH",
2384
- "country-code": "732"
2385
- },
2386
- {
2387
- "name": "Yemen",
2388
- "alpha-2": "YE",
2389
- "alpha-3": "YEM",
2390
- "country-code": "887"
2391
- },
2392
- {
2393
- "name": "Zambia",
2394
- "alpha-2": "ZM",
2395
- "alpha-3": "ZMB",
2396
- "country-code": "894"
2397
- },
2398
- {
2399
- "name": "Zimbabwe",
2400
- "alpha-2": "ZW",
2401
- "alpha-3": "ZWE",
2402
- "country-code": "716"
2403
- }
2404
- ];
2405
-
2406
- //#endregion
2407
- //#region src/api/search-analytics/breakdowns.ts
79
+ //#region src/api/sites.ts
2408
80
  /**
2409
- * Fetches device breakdown (desktop, mobile, tablet) with period comparison.
81
+ * Fetches all sites the authenticated user has access to in Google Search Console.
2410
82
  */
2411
- async function fetchDevicesWithComparison(client, siteUrl, options = {}) {
2412
- const [current, previous] = await Promise.all([client.searchAnalytics.query(siteUrl, {
2413
- ...createQueryBody(options),
2414
- dimensions: ["device"]
2415
- }).then((res) => {
2416
- return (res.rows || []).map((row) => {
2417
- return {
2418
- ...row,
2419
- dimension: "device",
2420
- device: row.keys?.[0] || "unknown",
2421
- keys: null,
2422
- clicks: Number(row.clicks) || 0,
2423
- impressions: Number(row.impressions) || 0,
2424
- ctr: Number(row.ctr) || 0,
2425
- position: Number(row.position) || 0
2426
- };
2427
- });
2428
- }), options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
2429
- ...createQueryBody({
2430
- ...options,
2431
- period: options.prevPeriod
2432
- }),
2433
- dimensions: ["device"]
2434
- }).then((res) => {
2435
- return (res.rows || []).map((row) => {
2436
- return {
2437
- ...row,
2438
- dimension: "device",
2439
- device: row.keys?.[0] || "unknown",
2440
- keys: null,
2441
- clicks: Number(row.clicks) || 0,
2442
- impressions: Number(row.impressions) || 0,
2443
- ctr: Number(row.ctr) || 0,
2444
- position: Number(row.position) || 0
2445
- };
2446
- });
2447
- }) : Promise.resolve([])]);
2448
- return {
2449
- current,
2450
- previous,
2451
- metadata: {
2452
- currentCount: current.length,
2453
- previousCount: previous.length
2454
- }
2455
- };
83
+ async function fetchSites(client) {
84
+ return client.sites();
2456
85
  }
2457
86
  /**
2458
- * Fetches device breakdown (desktop, mobile, tablet).
87
+ * Fetches all verified sites with their sitemaps from Google Search Console.
2459
88
  */
2460
- async function fetchDevices(client, siteUrl, options = {}) {
2461
- return client.searchAnalytics.query(siteUrl, {
2462
- ...createQueryBody(options),
2463
- dimensions: ["device"]
2464
- }).then((res) => {
2465
- return (res.rows || []).map((row) => {
2466
- return {
2467
- ...row,
2468
- dimension: "device",
2469
- device: row.keys?.[0] || "unknown",
2470
- keys: null,
2471
- clicks: Number(row.clicks) || 0,
2472
- impressions: Number(row.impressions) || 0,
2473
- ctr: Number(row.ctr) || 0,
2474
- position: Number(row.position) || 0
2475
- };
2476
- });
2477
- });
2478
- }
2479
- function fixCountryRows(res) {
2480
- return (res.rows || []).map((row) => {
2481
- const alpha3Code = row.keys?.[0] || "";
2482
- const country$1 = countries_default.find((c) => c["alpha-3"].toLowerCase() === alpha3Code);
89
+ async function fetchSitesWithSitemaps(client) {
90
+ const sites = (await client.sites()).filter((s) => !!s.siteUrl && s.permissionLevel !== "siteUnverifiedUser");
91
+ return Promise.all(sites.map(async (site) => {
92
+ const sitemaps = site.permissionLevel === "siteOwner" ? await client.sitemaps.list(site.siteUrl) : [];
2483
93
  return {
2484
- ...row,
2485
- dimension: "country",
2486
- countryCodeGsc: alpha3Code,
2487
- country: country$1?.name || alpha3Code,
2488
- countryCode: country$1?.["alpha-2"] || alpha3Code,
2489
- keys: null
94
+ ...site,
95
+ sitemaps
2490
96
  };
2491
- });
2492
- }
2493
- /**
2494
- * Fetches top countries by traffic with period comparison and keyword counts per country.
2495
- */
2496
- async function fetchCountriesWithComparison(client, siteUrl, options = {}) {
2497
- const [current, previous] = await Promise.all([client.searchAnalytics.query(siteUrl, {
2498
- ...createQueryBody(options),
2499
- dimensions: ["country"],
2500
- rowLimit: 5
2501
- }).then(fixCountryRows), options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
2502
- ...createQueryBody({
2503
- ...options,
2504
- period: options.prevPeriod
2505
- }),
2506
- dimensions: ["country"],
2507
- rowLimit: 5
2508
- }).then(fixCountryRows) : Promise.resolve([])]);
2509
- const keywordCounts = await Promise.all(current.map((row) => {
2510
- return client.searchAnalytics.query(siteUrl, {
2511
- ...createQueryBody({
2512
- ...options,
2513
- filters: [{
2514
- dimension: "country",
2515
- operator: "equals",
2516
- expression: row.countryCodeGsc
2517
- }]
2518
- }),
2519
- dimensions: ["query"]
2520
- }).then((res) => ({
2521
- countryCodeGsc: row.countryCodeGsc,
2522
- keywords: res.rows?.length || 0
2523
- })).catch(() => ({
2524
- countryCodeGsc: row.countryCodeGsc,
2525
- keywords: 0
2526
- }));
2527
97
  }));
2528
- for (const row of current) {
2529
- const keywordCount = keywordCounts.find((k) => k.countryCodeGsc === row.countryCodeGsc);
2530
- if (keywordCount) row.keywords = keywordCount.keywords;
2531
- }
2532
- return {
2533
- current,
2534
- previous,
2535
- metadata: {
2536
- currentCount: current.length,
2537
- previousCount: previous.length,
2538
- totalKeywords: keywordCounts.reduce((sum, k) => sum + k.keywords, 0)
2539
- }
2540
- };
2541
98
  }
2542
99
  /**
2543
- * Fetches top countries by traffic.
100
+ * Fetches all sitemaps for a site.
2544
101
  */
2545
- async function fetchCountries(client, siteUrl, options = {}) {
2546
- return client.searchAnalytics.query(siteUrl, {
2547
- ...createQueryBody(options),
2548
- dimensions: ["country"],
2549
- rowLimit: 5
2550
- }).then(fixCountryRows);
2551
- }
2552
- /**
2553
- * Fetches search appearance breakdown (AMP, rich results, etc.) with period comparison.
2554
- */
2555
- async function fetchSearchAppearanceWithComparison(client, siteUrl, options = {}) {
2556
- const [current, previous] = await Promise.all([client.searchAnalytics.query(siteUrl, {
2557
- ...createQueryBody(options),
2558
- dimensions: ["searchAppearance"]
2559
- }).then((res) => (res.rows || []).map((row) => ({
2560
- ...row,
2561
- dimension: "searchAppearance",
2562
- searchAppearance: row.keys?.[0] || "unknown",
2563
- keys: null,
2564
- clicks: Number(row.clicks) || 0,
2565
- impressions: Number(row.impressions) || 0,
2566
- ctr: Number(row.ctr) || 0,
2567
- position: Number(row.position) || 0
2568
- }))), options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
2569
- ...createQueryBody({
2570
- ...options,
2571
- period: options.prevPeriod
2572
- }),
2573
- dimensions: ["searchAppearance"]
2574
- }).then((res) => (res.rows || []).map((row) => ({
2575
- ...row,
2576
- dimension: "searchAppearance",
2577
- searchAppearance: row.keys?.[0] || "unknown",
2578
- keys: null,
2579
- clicks: Number(row.clicks) || 0,
2580
- impressions: Number(row.impressions) || 0,
2581
- ctr: Number(row.ctr) || 0,
2582
- position: Number(row.position) || 0
2583
- }))) : Promise.resolve([])]);
2584
- return {
2585
- current,
2586
- previous,
2587
- metadata: {
2588
- currentCount: current.length,
2589
- previousCount: previous.length
2590
- }
2591
- };
2592
- }
2593
- /**
2594
- * Fetches search appearance breakdown (AMP, rich results, etc.).
2595
- */
2596
- async function fetchSearchAppearance(client, siteUrl, options = {}) {
2597
- return client.searchAnalytics.query(siteUrl, {
2598
- ...createQueryBody(options),
2599
- dimensions: ["searchAppearance"]
2600
- }).then((res) => (res.rows || []).map((row) => ({
2601
- ...row,
2602
- dimension: "searchAppearance",
2603
- searchAppearance: row.keys?.[0] || "unknown",
2604
- keys: null,
2605
- clicks: Number(row.clicks) || 0,
2606
- impressions: Number(row.impressions) || 0,
2607
- ctr: Number(row.ctr) || 0,
2608
- position: Number(row.position) || 0
2609
- })));
2610
- }
2611
-
2612
- //#endregion
2613
- //#region src/api/search-analytics/dates.ts
2614
- function computeTotals(rows) {
2615
- if (!rows.length) return {
2616
- clicks: 0,
2617
- impressions: 0,
2618
- ctr: 0,
2619
- position: 0
2620
- };
2621
- return {
2622
- clicks: rows.reduce((sum, r) => sum + (r.clicks || 0), 0),
2623
- impressions: rows.reduce((sum, r) => sum + (r.impressions || 0), 0),
2624
- ctr: rows.reduce((sum, r) => sum + (r.ctr || 0), 0) / rows.length,
2625
- position: rows.reduce((sum, r) => sum + (r.position || 0), 0) / rows.length
2626
- };
102
+ async function fetchSitemaps(client, siteUrl) {
103
+ return client.sitemaps.list(siteUrl);
2627
104
  }
2628
105
  /**
2629
- * Fetches daily search analytics data with period-over-period comparison.
106
+ * Fetches a specific sitemap.
2630
107
  */
2631
- async function fetchDatesWithComparison(client, siteUrl, options = {}) {
2632
- const [current, previous] = await Promise.all([client.searchAnalytics.query(siteUrl, {
2633
- ...createQueryBody({ ...options }),
2634
- dimensions: ["date"]
2635
- }).then((res) => (res.rows || []).map((row) => ({
2636
- ...row,
2637
- dimension: "date",
2638
- date: row.keys?.[0] || "",
2639
- keys: null
2640
- }))), options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
2641
- ...createQueryBody({
2642
- ...options,
2643
- period: options.prevPeriod
2644
- }),
2645
- dimensions: ["date"]
2646
- }).then((res) => (res.rows || []).map((row) => ({
2647
- ...row,
2648
- dimension: "date",
2649
- date: row.keys?.[0] || "",
2650
- keys: null
2651
- }))) : Promise.resolve([])]);
2652
- const currentTotals = computeTotals(current);
2653
- const previousTotals = computeTotals(previous);
2654
- return {
2655
- current,
2656
- previous,
2657
- metadata: {
2658
- currentCount: current.length,
2659
- previousCount: previous.length,
2660
- totals: {
2661
- current: currentTotals,
2662
- previous: previousTotals,
2663
- clicksPercent: percentDifference(currentTotals.clicks, previousTotals.clicks),
2664
- impressionsPercent: percentDifference(currentTotals.impressions, previousTotals.impressions),
2665
- ctrPercent: percentDifference(currentTotals.ctr, previousTotals.ctr),
2666
- positionPercent: percentDifference(currentTotals.position, previousTotals.position)
2667
- }
2668
- }
2669
- };
108
+ async function fetchSitemap(client, siteUrl, feedpath) {
109
+ return client.sitemaps.get(siteUrl, feedpath);
2670
110
  }
2671
111
  /**
2672
- * Fetches year-over-year comparison for site metrics.
2673
- * GSC has ~16 months history, so works for periods up to ~4 months.
112
+ * Submits a sitemap to Google Search Console.
2674
113
  */
2675
- async function fetchYoYComparison(client, siteUrl, options = {}) {
2676
- const { period = {
2677
- start: dayjs().subtract(28, "day").toDate(),
2678
- end: dayjs().toDate()
2679
- } } = options;
2680
- const startDate = dayjs(period.start);
2681
- const endDate = dayjs(period.end);
2682
- const periodDays = endDate.diff(startDate, "day");
2683
- const prevStart = startDate.subtract(1, "year");
2684
- const prevEnd = endDate.subtract(1, "year");
2685
- const withinLimit = dayjs().diff(prevStart, "day") <= 480;
2686
- const [currentRes, previousRes] = await Promise.all([client.searchAnalytics.query(siteUrl, {
2687
- ...createQueryBody({ period }),
2688
- dimensions: ["date"]
2689
- }), withinLimit ? client.searchAnalytics.query(siteUrl, {
2690
- startDate: formatDateGsc(prevStart.toDate()) || "",
2691
- endDate: formatDateGsc(prevEnd.toDate()) || "",
2692
- dimensions: ["date"],
2693
- rowLimit: 25e3
2694
- }) : Promise.resolve({ rows: [] })]);
2695
- const currentRows = currentRes.rows || [];
2696
- const previousRows = previousRes.rows || [];
2697
- const sumMetrics = (rows) => {
2698
- if (!rows.length) return {
2699
- clicks: 0,
2700
- impressions: 0,
2701
- ctr: 0,
2702
- position: 0
2703
- };
2704
- return {
2705
- clicks: rows.reduce((sum, r) => sum + (r.clicks || 0), 0),
2706
- impressions: rows.reduce((sum, r) => sum + (r.impressions || 0), 0),
2707
- ctr: rows.reduce((sum, r) => sum + (r.ctr || 0), 0) / rows.length,
2708
- position: rows.reduce((sum, r) => sum + (r.position || 0), 0) / rows.length
2709
- };
2710
- };
2711
- const current = sumMetrics(currentRows);
2712
- const previous = sumMetrics(previousRows);
2713
- return {
2714
- current,
2715
- previous,
2716
- change: {
2717
- clicks: current.clicks - previous.clicks,
2718
- clicksPercent: percentDifference(current.clicks, previous.clicks),
2719
- impressions: current.impressions - previous.impressions,
2720
- impressionsPercent: percentDifference(current.impressions, previous.impressions),
2721
- ctr: current.ctr - previous.ctr,
2722
- ctrPercent: percentDifference(current.ctr, previous.ctr),
2723
- position: current.position - previous.position,
2724
- positionPercent: percentDifference(current.position, previous.position)
2725
- },
2726
- periodDays,
2727
- withinLimit
2728
- };
114
+ async function submitSitemap(client, siteUrl, feedpath) {
115
+ return client.sitemaps.submit(siteUrl, feedpath);
2729
116
  }
2730
117
  /**
2731
- * Fetches daily search analytics data for a site.
118
+ * Deletes a sitemap from Google Search Console.
2732
119
  */
2733
- async function fetchDates(client, siteUrl, options = {}) {
2734
- return await client.searchAnalytics.query(siteUrl, {
2735
- ...createQueryBody(options),
2736
- dimensions: ["date"]
2737
- }).then((res) => {
2738
- return (res.rows || []).map((row) => {
2739
- return {
2740
- ...row,
2741
- dimension: "date",
2742
- date: row.keys?.[0] || "",
2743
- keys: null
2744
- };
2745
- });
2746
- });
120
+ async function deleteSitemap(client, siteUrl, feedpath) {
121
+ return client.sitemaps.delete(siteUrl, feedpath);
2747
122
  }
2748
123
 
2749
124
  //#endregion
2750
- //#region src/api/search-analytics/utils.ts
2751
- function normalizePagePath(page$1, domain) {
2752
- if (!page$1) return page$1;
2753
- return page$1.replace("https://", "").replace(domain, "");
2754
- }
2755
- function extractDomain(siteUrl) {
2756
- return siteUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
2757
- }
2758
- function formatPageForQuery(page$1, siteUrl) {
2759
- let p = withBase(page$1, siteUrl);
2760
- if (p === extractDomain(siteUrl)) p = `${p}/`;
2761
- return withHttps(p);
125
+ //#region src/query/resolver.ts
126
+ const DATE_OPERATORS = [
127
+ "gte",
128
+ "gt",
129
+ "lte",
130
+ "lt",
131
+ "between"
132
+ ];
133
+ const METRIC_OPERATORS = [
134
+ "metricGte",
135
+ "metricGt",
136
+ "metricLte",
137
+ "metricLt",
138
+ "metricBetween"
139
+ ];
140
+ const SPECIAL_OPERATORS = ["topLevel"];
141
+ const QUERY_PARAMS = ["searchType"];
142
+ function isMetricOperator(op) {
143
+ return METRIC_OPERATORS.includes(op);
2762
144
  }
2763
-
2764
- //#endregion
2765
- //#region src/api/search-analytics/keywords.ts
2766
- /**
2767
- * Fetches keyword/query performance data with period comparison and associated pages.
2768
- */
2769
- async function fetchKeywordsWithComparison(client, siteUrl, options = {}) {
2770
- const [currentKeywords, previousKeywords, pages] = await Promise.all([
2771
- client.searchAnalytics.query(siteUrl, {
2772
- ...createQueryBody(options),
2773
- dimensions: ["query"]
2774
- }).then((res) => res.rows || []),
2775
- options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
2776
- ...createQueryBody({
2777
- ...options,
2778
- period: options.prevPeriod
2779
- }),
2780
- dimensions: ["query"]
2781
- }).then((res) => res.rows || []) : Promise.resolve([]),
2782
- client.searchAnalytics.query(siteUrl, {
2783
- ...createQueryBody(options),
2784
- dimensions: ["page", "query"]
2785
- }).then((res) => res.rows || [])
2786
- ]);
2787
- const current = currentKeywords.map((row) => {
2788
- const prevRow = previousKeywords.find((r) => r.keys?.[0] === row.keys?.[0]);
2789
- const pageMatch = pages.find((r) => r.keys?.[1] === row.keys?.[0]);
2790
- const position = row.position ?? 0;
2791
- const ctr = row.ctr ?? 0;
2792
- const prevPosition = prevRow?.position ?? 0;
2793
- const prevCtr = prevRow?.ctr ?? 0;
2794
- return {
2795
- ...row,
2796
- dimension: "query",
2797
- keyword: row.keys?.[0] || "",
2798
- page: pageMatch?.keys?.[0] ? normalizePagePath(pageMatch.keys[0], siteUrl) : null,
2799
- position,
2800
- positionPercent: percentDifference(position, prevPosition),
2801
- prevPosition,
2802
- ctr,
2803
- ctrPercent: percentDifference(ctr, prevCtr),
2804
- prevCtr,
2805
- clicks: row.clicks ?? 0,
2806
- impressions: row.impressions ?? 0,
2807
- keys: null
2808
- };
2809
- });
2810
- const previous = previousKeywords.map((prevRow) => {
2811
- const keywordKey = prevRow.keys?.[0] || "";
2812
- const currentRow = currentKeywords.find((r) => r.keys?.[0] === keywordKey);
2813
- const pageMatch = pages.find((r) => r.keys?.[1] === keywordKey);
2814
- if (!currentRow) return {
2815
- ...prevRow,
2816
- dimension: "query",
2817
- keyword: keywordKey,
2818
- page: pageMatch?.keys?.[0] ? normalizePagePath(pageMatch.keys[0], siteUrl) : null,
2819
- lost: true,
2820
- clicks: 0,
2821
- position: 0,
2822
- ctr: 0,
2823
- prevCtr: prevRow.ctr ?? 0,
2824
- prevPosition: prevRow.position ?? 0,
2825
- prevClicks: prevRow.clicks ?? 0,
2826
- prevImpressions: prevRow.impressions ?? 0,
2827
- keys: null
2828
- };
2829
- return {
2830
- ...prevRow,
2831
- dimension: "query",
2832
- keyword: keywordKey,
2833
- page: pageMatch?.keys?.[0] ? normalizePagePath(pageMatch.keys[0], siteUrl) : null,
2834
- keys: null
2835
- };
2836
- });
2837
- return {
2838
- current,
2839
- previous,
2840
- metadata: {
2841
- currentCount: current.length,
2842
- previousCount: previous.length,
2843
- pageMatches: pages.length
2844
- }
2845
- };
145
+ function isSpecialOperator(op) {
146
+ return SPECIAL_OPERATORS.includes(op);
2846
147
  }
2847
- /**
2848
- * Fetches all keywords with their performance data using recursive pagination.
2849
- */
2850
- async function fetchKeywords(client, siteUrl, options = {}) {
2851
- const { rows } = await queryRecursive(client, siteUrl, {
2852
- ...createQueryBody(options),
2853
- dimensions: ["query"]
2854
- });
2855
- return rows.map((row) => ({
2856
- ...row,
2857
- dimension: "query",
2858
- keyword: row.keys?.[0] || "",
2859
- keys: null
2860
- }));
148
+ function isDateOperator(op) {
149
+ return DATE_OPERATORS.includes(op);
2861
150
  }
2862
- /**
2863
- * Fetches detailed data for a specific keyword including daily trends and top pages.
2864
- */
2865
- async function fetchKeyword(client, siteUrl, keyword, options = {}) {
2866
- const { rowLimit = 5, ...queryOptions } = options;
2867
- const [dates, pages] = await Promise.all([client.searchAnalytics.query(siteUrl, {
2868
- ...createQueryBody({
2869
- ...queryOptions,
2870
- filters: [{
2871
- dimension: "query",
2872
- operator: "equals",
2873
- expression: keyword
2874
- }]
2875
- }),
2876
- dimensions: ["date"]
2877
- }).then((res) => (res.rows || []).map((row) => ({
2878
- ...row,
2879
- date: row.keys?.[0] ?? "",
2880
- keys: null
2881
- }))), client.searchAnalytics.query(siteUrl, {
2882
- ...createQueryBody({
2883
- ...queryOptions,
2884
- filters: [{
2885
- dimension: "query",
2886
- operator: "equals",
2887
- expression: keyword
2888
- }]
2889
- }),
2890
- rowLimit,
2891
- dimensions: ["page"]
2892
- }).then((res) => (res.rows || []).map((row) => ({
2893
- ...row,
2894
- page: normalizePagePath(row.keys?.[0] || "", extractDomain(siteUrl)),
2895
- keys: null
2896
- })))]);
2897
- return {
2898
- dates,
2899
- pages
2900
- };
151
+ function isQueryParam(dim) {
152
+ return QUERY_PARAMS.includes(dim);
2901
153
  }
2902
-
2903
- //#endregion
2904
- //#region src/api/search-analytics/pages.ts
2905
- /**
2906
- * Fetches all pages with their performance data using recursive pagination.
2907
- */
2908
- async function fetchPages(client, siteUrl, options = {}) {
2909
- return (await queryRecursive(client, siteUrl, {
2910
- ...createQueryBody(options),
2911
- dimensions: ["page"]
2912
- }).then((d) => d.rows)).map((row) => ({
2913
- ...row,
2914
- page: row.keys?.[0] || ""
2915
- }));
154
+ function addDays(dateStr, days) {
155
+ const d = new Date(dateStr);
156
+ d.setDate(d.getDate() + days);
157
+ return d.toISOString().split("T")[0];
2916
158
  }
2917
- /**
2918
- * Fetches page performance data with period comparison, including top keyword per page.
2919
- */
2920
- async function fetchPagesWithComparison(client, siteUrl, options = {}) {
2921
- const [currentPages, previousPages, keywords] = await Promise.all([
2922
- client.searchAnalytics.query(siteUrl, {
2923
- ...createQueryBody(options),
2924
- dimensions: ["page"]
2925
- }).then((res) => res.rows || []),
2926
- options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
2927
- ...createQueryBody({
2928
- ...options,
2929
- period: options.prevPeriod
2930
- }),
2931
- dimensions: ["page"]
2932
- }).then((res) => res.rows || []) : Promise.resolve([]),
2933
- client.searchAnalytics.query(siteUrl, {
2934
- ...createQueryBody(options),
2935
- dimensions: ["query", "page"]
2936
- }).then((res) => res.rows || [])
2937
- ]);
2938
- const current = currentPages.map((row) => {
2939
- const prevRow = previousPages.find((r) => r.keys?.[0] === row.keys?.[0]);
2940
- const keyword = keywords.find((r) => r.keys?.[1] === row.keys?.[0]);
2941
- const clicks = row.clicks ?? 0;
2942
- const impressions = row.impressions ?? 0;
2943
- const prevClicks = prevRow?.clicks ?? 0;
2944
- const prevImpressions = prevRow?.impressions ?? 0;
2945
- return {
2946
- ...row,
2947
- dimension: "page",
2948
- page: row.keys?.[0] || "",
2949
- keyword: keyword?.keys?.[0] || void 0,
2950
- keywordPosition: keyword?.position ?? 0,
2951
- clicks,
2952
- prevClicks,
2953
- clicksPercent: percentDifference(clicks, prevClicks),
2954
- impressions,
2955
- impressionsPercent: percentDifference(impressions, prevImpressions),
2956
- prevImpressions,
2957
- keys: null
2958
- };
2959
- });
2960
- const previous = previousPages.map((prevRow) => {
2961
- const pageKey = prevRow.keys?.[0] || "";
2962
- if (!currentPages.find((r) => r.keys?.[0] === pageKey)) return {
2963
- ...prevRow,
2964
- dimension: "page",
2965
- page: pageKey,
2966
- lost: true,
2967
- clicks: 0,
2968
- impressions: 0,
2969
- prevClicks: prevRow.clicks ?? 0,
2970
- prevImpressions: prevRow.impressions ?? 0,
2971
- keys: null
2972
- };
2973
- return {
2974
- ...prevRow,
2975
- dimension: "page",
2976
- page: pageKey,
2977
- keys: null
2978
- };
2979
- });
159
+ function extractSpecialFilters(filter) {
160
+ if (!filter) return {};
161
+ let startDate;
162
+ let endDate;
163
+ let searchType;
164
+ const otherFilters = [];
165
+ const cleanedNestedGroups = [];
166
+ for (const f of filter._filters) if (f.dimension === "date" && isDateOperator(f.operator)) switch (f.operator) {
167
+ case "gte":
168
+ startDate = f.expression;
169
+ break;
170
+ case "gt":
171
+ startDate = addDays(f.expression, 1);
172
+ break;
173
+ case "lte":
174
+ endDate = f.expression;
175
+ break;
176
+ case "lt":
177
+ endDate = addDays(f.expression, -1);
178
+ break;
179
+ case "between":
180
+ startDate = f.expression;
181
+ endDate = f.expression2;
182
+ break;
183
+ }
184
+ else if (isQueryParam(f.dimension)) {
185
+ if (f.dimension === "searchType") searchType = f.expression;
186
+ } else if (isMetricOperator(f.operator) || isSpecialOperator(f.operator)) otherFilters.push(f);
187
+ else otherFilters.push(f);
188
+ if (filter._nestedGroups) for (const nested of filter._nestedGroups) {
189
+ const extracted = extractSpecialFilters(nested);
190
+ if (extracted.startDate) startDate = extracted.startDate;
191
+ if (extracted.endDate) endDate = extracted.endDate;
192
+ if (extracted.searchType) searchType = extracted.searchType;
193
+ if (extracted.dimensionFilter) cleanedNestedGroups.push(extracted.dimensionFilter);
194
+ }
195
+ const dimensionFilter = otherFilters.length > 0 || cleanedNestedGroups.length > 0 ? {
196
+ ...filter,
197
+ _filters: otherFilters,
198
+ _nestedGroups: cleanedNestedGroups.length > 0 ? cleanedNestedGroups : void 0
199
+ } : void 0;
2980
200
  return {
2981
- current,
2982
- previous,
2983
- metadata: {
2984
- currentCount: current.length,
2985
- previousCount: previous.length,
2986
- keywordMatches: keywords.length
2987
- }
201
+ startDate,
202
+ endDate,
203
+ searchType,
204
+ dimensionFilter
2988
205
  };
2989
206
  }
2990
- /**
2991
- * Fetches detailed data for a specific page including daily trends and top keywords.
2992
- */
2993
- async function fetchPage(client, siteUrl, url, options = {}) {
2994
- const { rowLimit = 5, ...queryOptions } = options;
2995
- const [dates, keywords] = await Promise.all([client.searchAnalytics.query(siteUrl, {
2996
- ...createQueryBody({
2997
- ...queryOptions,
2998
- filters: [{
2999
- dimension: "page",
3000
- operator: "equals",
3001
- expression: formatPageForQuery(url, extractDomain(siteUrl))
3002
- }]
3003
- }),
3004
- dimensions: ["date"]
3005
- }).then((res) => (res.rows || []).map((row) => ({
3006
- ...row,
3007
- date: row.keys?.[0] ?? "",
3008
- keys: null
3009
- }))), client.searchAnalytics.query(siteUrl, {
3010
- ...createQueryBody({
3011
- ...queryOptions,
3012
- filters: [{
3013
- dimension: "page",
3014
- operator: "equals",
3015
- expression: formatPageForQuery(url, extractDomain(siteUrl))
3016
- }]
3017
- }),
3018
- rowLimit,
3019
- dimensions: ["query"]
3020
- }).then((res) => (res.rows || []).map((row) => ({
3021
- ...row,
3022
- keyword: row.keys?.[0] || "",
3023
- keys: null
3024
- })))]);
3025
- return {
3026
- dates,
3027
- keywords
207
+ function resolveToBody(state) {
208
+ const { startDate, endDate, searchType, dimensionFilter } = extractSpecialFilters(state.filter);
209
+ if (!startDate || !endDate) throw new Error("Date range required: use .where(between(date, start, end)) or .where(and(gte(date, start), lte(date, end)))");
210
+ const body = {
211
+ dimensions: state.dimensions,
212
+ startDate,
213
+ endDate
3028
214
  };
215
+ if (searchType) body.searchType = searchType;
216
+ if (state.rowLimit) body.rowLimit = state.rowLimit;
217
+ if (state.startRow) body.startRow = state.startRow;
218
+ const filterGroups = resolveFilter(dimensionFilter);
219
+ if (filterGroups.length > 0) body.dimensionFilterGroups = filterGroups;
220
+ return body;
221
+ }
222
+ function isApiFilter(f) {
223
+ return !isMetricOperator(f.operator) && !isSpecialOperator(f.operator);
224
+ }
225
+ function resolveFilter(filter) {
226
+ if (!filter) return [];
227
+ const groups = [];
228
+ const groupType = filter._groupType ?? "and";
229
+ const apiFilters = filter._filters.filter(isApiFilter);
230
+ if (groupType === "or") {
231
+ if (apiFilters.length > 0) groups.push({
232
+ groupType: "or",
233
+ filters: apiFilters.map((f) => ({
234
+ dimension: f.dimension,
235
+ operator: f.operator,
236
+ expression: f.expression
237
+ }))
238
+ });
239
+ } else if (apiFilters.length > 0) groups.push({ filters: apiFilters.map((f) => ({
240
+ dimension: f.dimension,
241
+ operator: f.operator,
242
+ expression: f.expression
243
+ })) });
244
+ if (filter._nestedGroups) for (const nested of filter._nestedGroups) groups.push(...resolveFilter(nested));
245
+ return groups;
3029
246
  }
3030
247
 
3031
248
  //#endregion
3032
- //#region src/api/search-analytics/stream.ts
3033
- const DIMENSION_TO_FIELD = {
3034
- query: "keyword",
3035
- page: "page",
3036
- date: "date",
3037
- device: "device",
3038
- country: "country"
3039
- };
3040
- function mapRowToDimensions(row, dimensions) {
3041
- const { keys, ...metrics } = row;
3042
- const fields = {};
3043
- dimensions.forEach((dim, i) => {
3044
- fields[DIMENSION_TO_FIELD[dim]] = keys?.[i] || "";
3045
- });
3046
- return {
3047
- ...metrics,
3048
- ...fields
3049
- };
3050
- }
249
+ //#region src/core/api-client.ts
3051
250
  /**
3052
- * Async generator for memory-efficient pagination of GSC search analytics.
3053
- * Yields batches of typed rows as they're fetched, avoiding accumulation in memory.
3054
- *
3055
- * **Design:** Single generic function with type inference replaces separate
3056
- * `fetchPagesStream`, `fetchKeywordsStream` wrappers. Output type is inferred
3057
- * from the dimensions tuple:
3058
- * - `['query']` → `{ keyword: string, clicks, impressions, ctr, position }`
3059
- * - `['page']` → `{ page: string, clicks, impressions, ctr, position }`
3060
- * - `['query', 'page']` → `{ keyword: string, page: string, ... }`
3061
- *
3062
- * Each yield contains up to 25,000 rows (one API page). Use `collectStream()`
3063
- * to gather all batches if full array is needed.
3064
- *
3065
- * @param client - GSC client instance
3066
- * @param siteUrl - Site URL (e.g., 'https://example.com/' or 'sc-domain:example.com')
3067
- * @param query - Query with dimensions array (requires `as const` for type inference)
251
+ * Create a client that queries GSC data through gscdump.com API
252
+ * instead of directly to Google. Useful when you don't have OAuth credentials.
3068
253
  *
3069
254
  * @example
3070
255
  * ```ts
3071
- * // Stream keyword+page combinations
3072
- * for await (const batch of queryRecursiveStream(client, url, {
3073
- * dimensions: ['query', 'page'] as const,
3074
- * startDate: '2024-01-01',
3075
- * endDate: '2024-01-31',
3076
- * })) {
3077
- * // batch: { keyword: string, page: string, clicks, impressions, ctr, position }[]
3078
- * await db.insert(batch)
3079
- * }
256
+ * const client = gscdumpApi({ apiKey: 'gsd_user_xxx' })
3080
257
  *
3081
- * // Or collect all at once
3082
- * const allRows = await collectStream(queryRecursiveStream(client, url, {
3083
- * dimensions: ['page'] as const,
3084
- * startDate: '2024-01-01',
3085
- * endDate: '2024-01-31',
3086
- * }))
258
+ * // Same query builder usage as direct client
259
+ * for await (const rows of client.query(siteId, gsc.select('page', 'query').where(...))) {
260
+ * console.log(rows)
261
+ * }
3087
262
  * ```
3088
- *
3089
- * @remarks
3090
- * - Requires `as const` on dimensions array for proper type inference
3091
- * - Use for large datasets (>25k rows) where memory is a concern
3092
- * - Non-paginating queries (devices, countries) don't benefit from streaming
3093
- */
3094
- async function* queryRecursiveStream(client, siteUrl, query$1) {
3095
- const rowLimit = query$1.rowLimit || 25e3;
3096
- let startRow = 0;
3097
- while (true) {
3098
- const rows = (await client.searchAnalytics.query(siteUrl, {
3099
- ...query$1,
3100
- dimensions: [...query$1.dimensions],
3101
- startRow,
3102
- rowLimit
3103
- })).rows || [];
3104
- if (rows.length === 0) break;
3105
- yield rows.map((row) => mapRowToDimensions(row, query$1.dimensions));
3106
- startRow += rows.length;
3107
- if (rows.length < rowLimit) break;
3108
- }
3109
- }
3110
- /** Collect all batches into single array (for testing / simple cases) */
3111
- async function collectStream(gen) {
3112
- const all = [];
3113
- for await (const batch of gen) all.push(...batch);
3114
- return all;
3115
- }
3116
-
3117
- //#endregion
3118
- //#region src/api/sites.ts
3119
- /**
3120
- * Fetches all sites the authenticated user has access to in Google Search Console.
3121
- */
3122
- async function fetchSites(client) {
3123
- return client.sites.list().then((res) => res?.siteEntry || []);
3124
- }
3125
- /**
3126
- * Fetches sitemaps for a site.
3127
- */
3128
- async function fetchSitemaps(client, siteUrl) {
3129
- return client.sitemaps.list(siteUrl).then((res) => res.sitemap || []);
3130
- }
3131
- /**
3132
- * Gets details for a specific sitemap.
3133
- */
3134
- async function fetchSitemap(client, siteUrl, feedpath) {
3135
- return client.sitemaps.get(siteUrl, feedpath);
3136
- }
3137
- /**
3138
- * Submits a sitemap to Google Search Console.
3139
- */
3140
- async function submitSitemap(client, siteUrl, feedpath) {
3141
- return client.sitemaps.submit(siteUrl, feedpath);
3142
- }
3143
- /**
3144
- * Deletes a sitemap from Google Search Console.
3145
- */
3146
- async function deleteSitemap(client, siteUrl, feedpath) {
3147
- return client.sitemaps.delete(siteUrl, feedpath);
3148
- }
3149
- /**
3150
- * Fetches all verified sites with their sitemaps from Google Search Console.
3151
263
  */
3152
- async function fetchSitesWithSitemaps(client) {
3153
- const sites = (await client.sites.list().then((res) => res.siteEntry || [])).filter((s) => !!s.siteUrl && s.permissionLevel !== "siteUnverifiedUser");
3154
- return Promise.all(sites.map(async (site) => {
3155
- const sitemaps = site.permissionLevel === "siteOwner" ? await client.sitemaps.list(site.siteUrl).then((res) => res.sitemap || []) : [];
3156
- return {
3157
- ...site,
3158
- sitemaps
3159
- };
3160
- }));
264
+ function gscdumpApi(options) {
265
+ const baseUrl = options.baseUrl?.replace(/\/$/, "") || "https://gscdump.com";
266
+ const fetch = ofetch.create({
267
+ baseURL: baseUrl,
268
+ retry: 3,
269
+ retryDelay: 1e3,
270
+ retryStatusCodes: [
271
+ 408,
272
+ 429,
273
+ 500,
274
+ 502,
275
+ 503,
276
+ 504
277
+ ],
278
+ headers: {
279
+ "x-api-key": options.apiKey,
280
+ "Content-Type": "application/json"
281
+ }
282
+ });
283
+ const rawQuery = async (siteId, body) => {
284
+ const response = await fetch(`/api/sites/${encodeURIComponent(siteId)}/query`, {
285
+ method: "POST",
286
+ body
287
+ });
288
+ return { rows: response.rows.map((row) => {
289
+ return {
290
+ keys: response.meta.dimensions.map((dim) => String(row[dim] ?? "")),
291
+ clicks: row.clicks,
292
+ impressions: row.impressions,
293
+ ctr: row.ctr,
294
+ position: row.position
295
+ };
296
+ }) };
297
+ };
298
+ return {
299
+ async *query(siteId, builder) {
300
+ const state = builder.getState();
301
+ const body = resolveToBody(state);
302
+ const rowLimit = body.rowLimit || 25e3;
303
+ let startRow = body.startRow || 0;
304
+ while (true) {
305
+ const response = await fetch(`/api/sites/${encodeURIComponent(siteId)}/query`, {
306
+ method: "POST",
307
+ body: {
308
+ ...body,
309
+ startRow,
310
+ rowLimit
311
+ }
312
+ });
313
+ const rows = response.rows.map((row) => {
314
+ const result = {
315
+ clicks: row.clicks ?? 0,
316
+ impressions: row.impressions ?? 0,
317
+ ctr: row.ctr ?? 0,
318
+ position: row.position ?? 0
319
+ };
320
+ state.dimensions.forEach((dim) => {
321
+ result[dim] = row[dim];
322
+ });
323
+ return result;
324
+ });
325
+ yield rows;
326
+ if (!response.meta.hasMore || rows.length < rowLimit) break;
327
+ startRow += rows.length;
328
+ }
329
+ },
330
+ sites: async () => {
331
+ return (await fetch("/api/sites")).sites.map((s) => ({
332
+ siteUrl: s.gscSiteUrl,
333
+ permissionLevel: s.permissionLevel || "siteOwner"
334
+ }));
335
+ },
336
+ inspect: () => {
337
+ throw new Error("URL inspection not available via gscdump API. Use googleSearchConsole() with OAuth credentials.");
338
+ },
339
+ sitemaps: {
340
+ list: async (siteId) => {
341
+ return (await fetch(`/api/sites/${encodeURIComponent(siteId)}/sitemaps`)).sitemaps || [];
342
+ },
343
+ get: () => {
344
+ throw new Error("Sitemap get not available via gscdump API.");
345
+ },
346
+ submit: () => {
347
+ throw new Error("Sitemap submit not available via gscdump API.");
348
+ },
349
+ delete: () => {
350
+ throw new Error("Sitemap delete not available via gscdump API.");
351
+ }
352
+ },
353
+ indexing: {
354
+ publish: () => {
355
+ throw new Error("Indexing API not available via gscdump API. Use googleSearchConsole() with OAuth credentials.");
356
+ },
357
+ getMetadata: () => {
358
+ throw new Error("Indexing API not available via gscdump API. Use googleSearchConsole() with OAuth credentials.");
359
+ }
360
+ },
361
+ _rawQuery: rawQuery
362
+ };
3161
363
  }
3162
364
 
3163
365
  //#endregion
@@ -3251,25 +453,56 @@ function googleSearchConsole(auth, options = {}) {
3251
453
  }
3252
454
  fetch = createFetch(authState, fetchOptions);
3253
455
  }
456
+ const rawQuery = (siteUrl, body) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`, {
457
+ method: "POST",
458
+ body
459
+ });
3254
460
  return {
3255
- sites: { list: () => fetch(`${GSC_API}/webmasters/v3/sites`) },
3256
- sitemaps: {
3257
- list: (siteUrl) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps`),
3258
- get: (siteUrl, feedpath) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`),
3259
- submit: (siteUrl, feedpath) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, { method: "PUT" }),
3260
- delete: (siteUrl, feedpath) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, { method: "DELETE" })
461
+ async *query(siteUrl, builder) {
462
+ const state = builder.getState();
463
+ const body = resolveToBody(state);
464
+ const rowLimit = body.rowLimit || 25e3;
465
+ let startRow = body.startRow || 0;
466
+ while (true) {
467
+ const rows = ((await rawQuery(siteUrl, {
468
+ ...body,
469
+ startRow,
470
+ rowLimit
471
+ })).rows || []).map((row) => {
472
+ const result = {
473
+ clicks: row.clicks ?? 0,
474
+ impressions: row.impressions ?? 0,
475
+ ctr: row.ctr ?? 0,
476
+ position: row.position ?? 0
477
+ };
478
+ state.dimensions.forEach((dim, i) => {
479
+ result[dim] = row.keys?.[i];
480
+ });
481
+ return result;
482
+ });
483
+ yield rows;
484
+ if (rows.length < rowLimit) break;
485
+ startRow += rows.length;
486
+ }
3261
487
  },
3262
- searchAnalytics: { query: (siteUrl, body) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`, {
3263
- method: "POST",
3264
- body
3265
- }) },
3266
- urlInspection: { inspect: (siteUrl, inspectionUrl) => fetch(`${GSC_API}/v1/urlInspection/index:inspect`, {
488
+ sites: async () => {
489
+ return (await fetch(`${GSC_API}/webmasters/v3/sites`)).siteEntry || [];
490
+ },
491
+ inspect: (siteUrl, url) => fetch(`${GSC_API}/v1/urlInspection/index:inspect`, {
3267
492
  method: "POST",
3268
493
  body: {
3269
- inspectionUrl,
494
+ inspectionUrl: url,
3270
495
  siteUrl
3271
496
  }
3272
- }) },
497
+ }),
498
+ sitemaps: {
499
+ list: async (siteUrl) => {
500
+ return (await fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps`)).sitemap || [];
501
+ },
502
+ get: (siteUrl, feedpath) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`),
503
+ submit: (siteUrl, feedpath) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, { method: "PUT" }),
504
+ delete: (siteUrl, feedpath) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, { method: "DELETE" })
505
+ },
3273
506
  indexing: {
3274
507
  publish: (url, type) => fetch(`${INDEXING_API}/v3/urlNotifications:publish`, {
3275
508
  method: "POST",
@@ -3279,7 +512,8 @@ function googleSearchConsole(auth, options = {}) {
3279
512
  }
3280
513
  }),
3281
514
  getMetadata: (url) => fetch(`${INDEXING_API}/v3/urlNotifications/metadata`, { query: { url } })
3282
- }
515
+ },
516
+ _rawQuery: rawQuery
3283
517
  };
3284
518
  }
3285
519
 
@@ -3430,361 +664,4 @@ function formatErrorForCli(error) {
3430
664
  }
3431
665
 
3432
666
  //#endregion
3433
- //#region src/query/resolver.ts
3434
- const DATE_OPERATORS = [
3435
- "gte",
3436
- "gt",
3437
- "lte",
3438
- "lt",
3439
- "between"
3440
- ];
3441
- function isDateOperator(op) {
3442
- return DATE_OPERATORS.includes(op);
3443
- }
3444
- function addDays(dateStr, days) {
3445
- const d = new Date(dateStr);
3446
- d.setDate(d.getDate() + days);
3447
- return d.toISOString().split("T")[0];
3448
- }
3449
- /**
3450
- * Extract date range from filters. Used by analysis functions to get the period.
3451
- */
3452
- function extractDateRange(filters) {
3453
- const { startDate, endDate } = extractDateFilters(filters);
3454
- return {
3455
- startDate,
3456
- endDate
3457
- };
3458
- }
3459
- function extractDateFilters(filters) {
3460
- let startDate;
3461
- let endDate;
3462
- const nonDateFilters = [];
3463
- for (const filter of filters) {
3464
- const dateFilters = [];
3465
- const otherFilters = [];
3466
- for (const f of filter._filters) if (f.dimension === "date" && isDateOperator(f.operator)) dateFilters.push(f);
3467
- else otherFilters.push(f);
3468
- for (const df of dateFilters) switch (df.operator) {
3469
- case "gte":
3470
- startDate = df.expression;
3471
- break;
3472
- case "gt":
3473
- startDate = addDays(df.expression, 1);
3474
- break;
3475
- case "lte":
3476
- endDate = df.expression;
3477
- break;
3478
- case "lt":
3479
- endDate = addDays(df.expression, -1);
3480
- break;
3481
- case "between":
3482
- startDate = df.expression;
3483
- endDate = df.expression2;
3484
- break;
3485
- }
3486
- if (otherFilters.length > 0) nonDateFilters.push({
3487
- ...filter,
3488
- _filters: otherFilters
3489
- });
3490
- }
3491
- return {
3492
- startDate,
3493
- endDate,
3494
- nonDateFilters
3495
- };
3496
- }
3497
- function resolveToBody(state) {
3498
- const { startDate, endDate, nonDateFilters } = extractDateFilters(state.filters);
3499
- if (!startDate || !endDate) throw new Error("Date range required: use .where(between(date, start, end)) or .where(gte(date, start)).where(lte(date, end))");
3500
- const body = {
3501
- dimensions: state.dimensions,
3502
- startDate,
3503
- endDate
3504
- };
3505
- if (state.rowLimit) body.rowLimit = state.rowLimit;
3506
- if (state.startRow) body.startRow = state.startRow;
3507
- const filterGroups = resolveFilters(nonDateFilters);
3508
- if (filterGroups.length > 0) body.dimensionFilterGroups = filterGroups;
3509
- return body;
3510
- }
3511
- function resolveFilters(filters) {
3512
- const groups = [];
3513
- for (const filter of filters) if ((filter._groupType ?? "and") === "or" && filter._filters.length > 1) groups.push({
3514
- groupType: "or",
3515
- filters: filter._filters.map((f) => ({
3516
- dimension: f.dimension,
3517
- operator: f.operator,
3518
- expression: f.expression
3519
- }))
3520
- });
3521
- else if (filter._filters.length > 0) groups.push({ filters: filter._filters.map((f) => ({
3522
- dimension: f.dimension,
3523
- operator: f.operator,
3524
- expression: f.expression
3525
- })) });
3526
- return groups;
3527
- }
3528
-
3529
- //#endregion
3530
- //#region src/query/builder.ts
3531
- function transformResponse(response, dimensions) {
3532
- return { rows: (response.rows ?? []).map((row) => {
3533
- const result = {
3534
- clicks: row.clicks,
3535
- impressions: row.impressions,
3536
- ctr: row.ctr,
3537
- position: row.position
3538
- };
3539
- dimensions.forEach((dim, i) => {
3540
- result[dim] = row.keys?.[i];
3541
- });
3542
- return result;
3543
- }) };
3544
- }
3545
- function createBuilder(state) {
3546
- return {
3547
- select(...dims) {
3548
- return createBuilder({
3549
- ...state,
3550
- dimensions: dims
3551
- });
3552
- },
3553
- where(filter) {
3554
- return createBuilder({
3555
- ...state,
3556
- filters: [...state.filters, filter]
3557
- });
3558
- },
3559
- siteUrl(url) {
3560
- return createBuilder({
3561
- ...state,
3562
- siteUrl: url
3563
- });
3564
- },
3565
- limit(n) {
3566
- return createBuilder({
3567
- ...state,
3568
- rowLimit: n
3569
- });
3570
- },
3571
- offset(n) {
3572
- return createBuilder({
3573
- ...state,
3574
- startRow: n
3575
- });
3576
- },
3577
- async execute(client) {
3578
- const body = resolveToBody(state);
3579
- return transformResponse(await client.searchAnalytics.query(state.siteUrl, body), state.dimensions);
3580
- },
3581
- toBody() {
3582
- return resolveToBody(state);
3583
- },
3584
- getState() {
3585
- return { ...state };
3586
- }
3587
- };
3588
- }
3589
- const gsc = createBuilder({
3590
- dimensions: [],
3591
- filters: []
3592
- });
3593
-
3594
- //#endregion
3595
- //#region src/query/columns.ts
3596
- function createColumn(dimension) {
3597
- return { dimension };
3598
- }
3599
- const page = createColumn("page");
3600
- const query = createColumn("query");
3601
- const device = createColumn("device");
3602
- const country = createColumn("country");
3603
- const searchAppearance = createColumn("searchAppearance");
3604
- const date = createColumn("date");
3605
-
3606
- //#endregion
3607
- //#region src/query/constants.ts
3608
- const Device = {
3609
- MOBILE: "MOBILE",
3610
- DESKTOP: "DESKTOP",
3611
- TABLET: "TABLET"
3612
- };
3613
- const Country = Object.fromEntries(countries_default.map((c) => [c["alpha-3"], c["alpha-3"].toLowerCase()]));
3614
-
3615
- //#endregion
3616
- //#region src/query/operators.ts
3617
- function eq(column, value) {
3618
- return {
3619
- _constraints: {},
3620
- _filters: [{
3621
- dimension: column.dimension,
3622
- operator: "equals",
3623
- expression: String(value)
3624
- }]
3625
- };
3626
- }
3627
- function ne(column, value) {
3628
- return {
3629
- _constraints: {},
3630
- _filters: [{
3631
- dimension: column.dimension,
3632
- operator: "notEquals",
3633
- expression: String(value)
3634
- }]
3635
- };
3636
- }
3637
- function inArray(column, values) {
3638
- return {
3639
- _constraints: {},
3640
- _filters: values.map((v) => ({
3641
- dimension: column.dimension,
3642
- operator: "equals",
3643
- expression: String(v)
3644
- })),
3645
- _groupType: "or"
3646
- };
3647
- }
3648
- function contains(column, pattern) {
3649
- return {
3650
- _constraints: {},
3651
- _filters: [{
3652
- dimension: column.dimension,
3653
- operator: "contains",
3654
- expression: pattern
3655
- }]
3656
- };
3657
- }
3658
- function like(column, pattern) {
3659
- return {
3660
- _constraints: {},
3661
- _filters: [{
3662
- dimension: column.dimension,
3663
- operator: "contains",
3664
- expression: pattern.replace(/%/g, "")
3665
- }]
3666
- };
3667
- }
3668
- function regex(column, pattern) {
3669
- return {
3670
- _constraints: {},
3671
- _filters: [{
3672
- dimension: column.dimension,
3673
- operator: "includingRegex",
3674
- expression: typeof pattern === "string" ? pattern : pattern.source
3675
- }]
3676
- };
3677
- }
3678
- function notRegex(column, pattern) {
3679
- return {
3680
- _constraints: {},
3681
- _filters: [{
3682
- dimension: column.dimension,
3683
- operator: "excludingRegex",
3684
- expression: typeof pattern === "string" ? pattern : pattern.source
3685
- }]
3686
- };
3687
- }
3688
- function and(...filters) {
3689
- return {
3690
- _constraints: {},
3691
- _filters: filters.flatMap((f) => f._filters),
3692
- _groupType: "and"
3693
- };
3694
- }
3695
- function or(...filters) {
3696
- return {
3697
- _constraints: {},
3698
- _filters: filters.flatMap((f) => f._filters),
3699
- _groupType: "or"
3700
- };
3701
- }
3702
- const DATE_OPS = [
3703
- "gte",
3704
- "gt",
3705
- "lte",
3706
- "lt",
3707
- "between"
3708
- ];
3709
- function not(filter) {
3710
- return {
3711
- _constraints: {},
3712
- _filters: filter._filters.filter((f) => !DATE_OPS.includes(f.operator)).map((f) => ({
3713
- ...f,
3714
- operator: invertOperator(f.operator)
3715
- }))
3716
- };
3717
- }
3718
- function invertOperator(op) {
3719
- return {
3720
- equals: "notEquals",
3721
- notEquals: "equals",
3722
- contains: "notContains",
3723
- notContains: "contains",
3724
- includingRegex: "excludingRegex",
3725
- excludingRegex: "includingRegex"
3726
- }[op];
3727
- }
3728
- function gte(column, value) {
3729
- return {
3730
- _constraints: {},
3731
- _filters: [{
3732
- dimension: column.dimension,
3733
- operator: "gte",
3734
- expression: String(value)
3735
- }]
3736
- };
3737
- }
3738
- function gt(column, value) {
3739
- return {
3740
- _constraints: {},
3741
- _filters: [{
3742
- dimension: column.dimension,
3743
- operator: "gt",
3744
- expression: String(value)
3745
- }]
3746
- };
3747
- }
3748
- function lte(column, value) {
3749
- return {
3750
- _constraints: {},
3751
- _filters: [{
3752
- dimension: column.dimension,
3753
- operator: "lte",
3754
- expression: String(value)
3755
- }]
3756
- };
3757
- }
3758
- function lt(column, value) {
3759
- return {
3760
- _constraints: {},
3761
- _filters: [{
3762
- dimension: column.dimension,
3763
- operator: "lt",
3764
- expression: String(value)
3765
- }]
3766
- };
3767
- }
3768
- function between(column, start, end) {
3769
- return {
3770
- _constraints: {},
3771
- _filters: [{
3772
- dimension: column.dimension,
3773
- operator: "between",
3774
- expression: String(start),
3775
- expression2: String(end)
3776
- }]
3777
- };
3778
- }
3779
-
3780
- //#endregion
3781
- //#region src/query/index.ts
3782
- function today() {
3783
- return currentPstDate();
3784
- }
3785
- function daysAgo(n) {
3786
- return dayjsPst().subtract(n, "day").format("YYYY-MM-DD");
3787
- }
3788
-
3789
- //#endregion
3790
- export { Country, Device, GSC_QUOTAS, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeError, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, and, batchInspectUrls, batchRequestIndexing, between, collectStream, contains, countries_default as countries, country, createAuth, createFetch, createQueryBody, createSorter, currentPstDate, date, dayjs, dayjsPst, daysAgo, deleteSitemap, device, eq, extractDateRange, fetchAnalyticsWithComparison, fetchCountries, fetchCountriesWithComparison, fetchDates, fetchDatesWithComparison, fetchDevices, fetchDevicesWithComparison, fetchKeyword, fetchKeywords, fetchKeywordsWithComparison, fetchPage, fetchPages, fetchPagesWithComparison, fetchSearchAppearance, fetchSearchAppearanceWithComparison, fetchSitemap, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, fetchYoYComparison, formatDateGsc, formatErrorForCli, getErrorCode, getErrorMessage, getIndexingMetadata, getRetryAfter, googleSearchConsole, gsc, gt, gte, inArray, inspectUrl, isAuthError, isQuotaError, isRateLimitError, like, lt, lte, ne, not, notRegex, num, or, page, percentDifference, query, queryRecursive, queryRecursiveStream, regex, requestIndexing, searchAppearance, submitSitemap, today, withDataType, withFinalData, withFreshData, withPropertyAggregation, withSearchAppearance };
667
+ export { GSC_QUOTAS, analyzeError, batchInspectUrls, batchRequestIndexing, createAuth, createFetch, deleteSitemap, fetchSitemap, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, formatErrorForCli, getErrorCode, getErrorMessage, getIndexingMetadata, getRetryAfter, googleSearchConsole, gscdumpApi, inspectUrl, isAuthError, isQuotaError, isRateLimitError, requestIndexing, submitSitemap };