gscdump 0.0.1 → 0.1.2
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/README.md +172 -0
- package/dist/index.d.mts +229 -249
- package/dist/index.mjs +358 -400
- package/dist/query/index.d.mts +157 -0
- package/dist/query/index.mjs +1848 -0
- package/package.json +16 -7
package/dist/index.mjs
CHANGED
|
@@ -17,134 +17,6 @@ function createSorter(getValue, defaultMetric, defaultOrder = "desc") {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
//#endregion
|
|
21
|
-
//#region src/analysis/striking-distance.ts
|
|
22
|
-
const sortResults$4 = createSorter((item, metric) => item[metric], "potentialClicks");
|
|
23
|
-
/**
|
|
24
|
-
* Finds striking distance keywords - high impressions, low CTR, position 4-20.
|
|
25
|
-
* These are "quick wins" that could gain significant traffic with small ranking improvements.
|
|
26
|
-
*/
|
|
27
|
-
function analyzeStrikingDistance(keywords, options = {}) {
|
|
28
|
-
const { minPosition = 4, maxPosition = 20, minImpressions = 100, maxCtr = .05, sortBy = "potentialClicks", sortOrder = "desc" } = options;
|
|
29
|
-
const results = [];
|
|
30
|
-
for (const row of keywords) {
|
|
31
|
-
const position = num(row.position);
|
|
32
|
-
const impressions = num(row.impressions);
|
|
33
|
-
const ctr = num(row.ctr);
|
|
34
|
-
const clicks = num(row.clicks);
|
|
35
|
-
if (position < minPosition || position > maxPosition) continue;
|
|
36
|
-
if (impressions < minImpressions) continue;
|
|
37
|
-
if (ctr > maxCtr) continue;
|
|
38
|
-
const potentialClicks = Math.round(impressions * .15);
|
|
39
|
-
results.push({
|
|
40
|
-
keyword: row.keyword,
|
|
41
|
-
page: row.page ?? null,
|
|
42
|
-
clicks,
|
|
43
|
-
impressions,
|
|
44
|
-
ctr,
|
|
45
|
-
position,
|
|
46
|
-
potentialClicks
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
return sortResults$4(results, sortBy, sortOrder);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
//#endregion
|
|
53
|
-
//#region src/analysis/opportunity.ts
|
|
54
|
-
const EXPECTED_CTR_BY_POSITION = {
|
|
55
|
-
1: .3,
|
|
56
|
-
2: .15,
|
|
57
|
-
3: .1,
|
|
58
|
-
4: .07,
|
|
59
|
-
5: .05,
|
|
60
|
-
6: .04,
|
|
61
|
-
7: .03,
|
|
62
|
-
8: .025,
|
|
63
|
-
9: .02,
|
|
64
|
-
10: .015
|
|
65
|
-
};
|
|
66
|
-
function getExpectedCtr(position) {
|
|
67
|
-
return EXPECTED_CTR_BY_POSITION[Math.round(Math.max(1, Math.min(position, 10)))] || .01;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Position score: higher for positions 4-20 (improvable range).
|
|
71
|
-
* Peak opportunity at positions 8-15.
|
|
72
|
-
*/
|
|
73
|
-
function calculatePositionScore(position) {
|
|
74
|
-
if (position <= 3) return .2;
|
|
75
|
-
if (position > 50) return .1;
|
|
76
|
-
const distance = Math.abs(position - 11);
|
|
77
|
-
return Math.max(0, 1 - distance / 15);
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Impression score using log scale.
|
|
81
|
-
* Higher impressions = more traffic potential.
|
|
82
|
-
*/
|
|
83
|
-
function calculateImpressionScore(impressions) {
|
|
84
|
-
if (impressions <= 0) return 0;
|
|
85
|
-
return Math.min(Math.log10(impressions) / 5, 1);
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* CTR gap score: difference between actual and expected CTR.
|
|
89
|
-
* Higher gap = more opportunity for improvement.
|
|
90
|
-
*/
|
|
91
|
-
function calculateCtrGapScore(actualCtr, position) {
|
|
92
|
-
const expectedCtr = getExpectedCtr(position);
|
|
93
|
-
if (actualCtr >= expectedCtr) return 0;
|
|
94
|
-
const gap = expectedCtr - actualCtr;
|
|
95
|
-
return Math.min(gap / expectedCtr, 1);
|
|
96
|
-
}
|
|
97
|
-
const SORT_ORDER$1 = {
|
|
98
|
-
opportunityScore: "desc",
|
|
99
|
-
potentialClicks: "desc",
|
|
100
|
-
impressions: "desc",
|
|
101
|
-
position: "asc"
|
|
102
|
-
};
|
|
103
|
-
const sortResults$3 = createSorter((item, metric) => item[metric], "opportunityScore");
|
|
104
|
-
/**
|
|
105
|
-
* Scores keywords by optimization opportunity.
|
|
106
|
-
* Composite score combining position, impressions, and CTR gap factors.
|
|
107
|
-
*/
|
|
108
|
-
function analyzeOpportunity(keywords, options = {}) {
|
|
109
|
-
const { minImpressions = 100, weights = {}, sortBy = "opportunityScore" } = options;
|
|
110
|
-
const positionWeight = weights.position ?? 1;
|
|
111
|
-
const impressionsWeight = weights.impressions ?? 1;
|
|
112
|
-
const ctrGapWeight = weights.ctrGap ?? 1;
|
|
113
|
-
const results = [];
|
|
114
|
-
for (const row of keywords) {
|
|
115
|
-
const impressions = num(row.impressions);
|
|
116
|
-
const position = num(row.position);
|
|
117
|
-
const ctr = num(row.ctr);
|
|
118
|
-
const clicks = num(row.clicks);
|
|
119
|
-
if (impressions < minImpressions) continue;
|
|
120
|
-
const positionScore = calculatePositionScore(position);
|
|
121
|
-
const impressionScore = calculateImpressionScore(impressions);
|
|
122
|
-
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
123
|
-
const weightedProduct = positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight;
|
|
124
|
-
const totalWeight = positionWeight + impressionsWeight + ctrGapWeight;
|
|
125
|
-
const geometricMean = Math.pow(weightedProduct, 1 / totalWeight);
|
|
126
|
-
const opportunityScore = Math.round(geometricMean * 100);
|
|
127
|
-
const targetCtr = getExpectedCtr(Math.min(3, position));
|
|
128
|
-
const potentialClicks = Math.round(impressions * targetCtr);
|
|
129
|
-
results.push({
|
|
130
|
-
keyword: row.keyword,
|
|
131
|
-
page: row.page ?? null,
|
|
132
|
-
clicks,
|
|
133
|
-
impressions,
|
|
134
|
-
ctr,
|
|
135
|
-
position,
|
|
136
|
-
opportunityScore,
|
|
137
|
-
potentialClicks,
|
|
138
|
-
factors: {
|
|
139
|
-
positionScore,
|
|
140
|
-
impressionScore,
|
|
141
|
-
ctrGapScore
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
return sortResults$3(results, sortBy, SORT_ORDER$1[sortBy]);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
20
|
//#endregion
|
|
149
21
|
//#region src/analysis/brand.ts
|
|
150
22
|
/**
|
|
@@ -177,6 +49,153 @@ function analyzeBrandSegmentation(keywords, options) {
|
|
|
177
49
|
};
|
|
178
50
|
}
|
|
179
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
|
+
|
|
180
199
|
//#endregion
|
|
181
200
|
//#region src/analysis/concentration.ts
|
|
182
201
|
/**
|
|
@@ -270,12 +289,12 @@ function analyzeKeywordConcentration(keywords, options) {
|
|
|
270
289
|
|
|
271
290
|
//#endregion
|
|
272
291
|
//#region src/analysis/decay.ts
|
|
273
|
-
const SORT_ORDER = {
|
|
292
|
+
const SORT_ORDER$1 = {
|
|
274
293
|
lostClicks: "desc",
|
|
275
294
|
declinePercent: "desc",
|
|
276
295
|
currentClicks: "asc"
|
|
277
296
|
};
|
|
278
|
-
const sortResults$
|
|
297
|
+
const sortResults$3 = createSorter((item, metric) => item[metric], "lostClicks");
|
|
279
298
|
/**
|
|
280
299
|
* Identifies "decaying" content - pages that have lost significant traffic.
|
|
281
300
|
* Useful for finding old content that needs updating.
|
|
@@ -317,7 +336,7 @@ function analyzeDecay(input, options = {}) {
|
|
|
317
336
|
positionDrop: curr.position - prev.position
|
|
318
337
|
});
|
|
319
338
|
}
|
|
320
|
-
return sortResults$
|
|
339
|
+
return sortResults$3(results, sortBy, SORT_ORDER$1[sortBy]);
|
|
321
340
|
}
|
|
322
341
|
|
|
323
342
|
//#endregion
|
|
@@ -402,84 +421,97 @@ function analyzeMovers(input, options = {}) {
|
|
|
402
421
|
}
|
|
403
422
|
|
|
404
423
|
//#endregion
|
|
405
|
-
//#region src/analysis/
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
+
}
|
|
414
440
|
/**
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
* @param rows Query+page data rows
|
|
419
|
-
* @param options Filtering and sorting options
|
|
441
|
+
* Position score: higher for positions 4-20 (improvable range).
|
|
442
|
+
* Peak opportunity at positions 8-15.
|
|
420
443
|
*/
|
|
421
|
-
function
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const pages = queryMap.get(row.query) || [];
|
|
427
|
-
pages.push({
|
|
428
|
-
page: row.page,
|
|
429
|
-
clicks: row.clicks,
|
|
430
|
-
impressions: row.impressions,
|
|
431
|
-
ctr: row.ctr,
|
|
432
|
-
position: row.position
|
|
433
|
-
});
|
|
434
|
-
queryMap.set(row.query, pages);
|
|
435
|
-
}
|
|
436
|
-
const results = [];
|
|
437
|
-
for (const [query$1, pages] of queryMap) {
|
|
438
|
-
if (pages.length < minPages) continue;
|
|
439
|
-
pages.sort((a, b) => b.clicks - a.clicks);
|
|
440
|
-
const positions = pages.map((p) => p.position);
|
|
441
|
-
const positionSpread = Math.max(...positions) - Math.min(...positions);
|
|
442
|
-
if (positionSpread > maxPositionSpread) continue;
|
|
443
|
-
results.push({
|
|
444
|
-
query: query$1,
|
|
445
|
-
pages,
|
|
446
|
-
totalClicks: pages.reduce((sum, p) => sum + p.clicks, 0),
|
|
447
|
-
totalImpressions: pages.reduce((sum, p) => sum + p.impressions, 0),
|
|
448
|
-
positionSpread
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
return sortResults$1(results, sortBy, sortOrder);
|
|
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);
|
|
452
449
|
}
|
|
453
|
-
|
|
454
|
-
//#endregion
|
|
455
|
-
//#region src/analysis/zero-click.ts
|
|
456
|
-
const sortResults = createSorter((item) => item.impressions, "impressions");
|
|
457
450
|
/**
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
* often due to SERP features (Answer Boxes, Knowledge Panels, AI Overviews).
|
|
461
|
-
*
|
|
462
|
-
* @param rows Query+page data rows
|
|
463
|
-
* @param options Filtering options
|
|
451
|
+
* Impression score using log scale.
|
|
452
|
+
* Higher impressions = more traffic potential.
|
|
464
453
|
*/
|
|
465
|
-
function
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
+
}
|
|
480
512
|
});
|
|
481
513
|
}
|
|
482
|
-
return sortResults(
|
|
514
|
+
return sortResults$2(results, sortBy, SORT_ORDER[sortBy]);
|
|
483
515
|
}
|
|
484
516
|
|
|
485
517
|
//#endregion
|
|
@@ -549,100 +581,66 @@ function analyzeSeasonality(dates, options = {}) {
|
|
|
549
581
|
}
|
|
550
582
|
|
|
551
583
|
//#endregion
|
|
552
|
-
//#region src/analysis/
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
"
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
return
|
|
581
|
-
}
|
|
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");
|
|
582
618
|
/**
|
|
583
|
-
*
|
|
584
|
-
*
|
|
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).
|
|
585
622
|
*
|
|
586
|
-
* @param
|
|
587
|
-
* @param options
|
|
623
|
+
* @param rows Query+page data rows
|
|
624
|
+
* @param options Filtering options
|
|
588
625
|
*/
|
|
589
|
-
function
|
|
590
|
-
const {
|
|
591
|
-
const
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (clusterBy === "prefix" || clusterBy === "both") {
|
|
607
|
-
const unclustered = filtered.filter((kw) => !clusteredKeywords.has(kw.keyword));
|
|
608
|
-
const prefixMap = /* @__PURE__ */ new Map();
|
|
609
|
-
for (const kw of unclustered) {
|
|
610
|
-
const prefix = extractWordPrefix(kw.keyword);
|
|
611
|
-
if (prefix) {
|
|
612
|
-
const existing = prefixMap.get(prefix);
|
|
613
|
-
if (existing) existing.push(kw);
|
|
614
|
-
else prefixMap.set(prefix, [kw]);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
for (const [prefix, kws] of prefixMap) if (kws.length >= minClusterSize) {
|
|
618
|
-
clusterMap.set(prefix, {
|
|
619
|
-
type: "prefix",
|
|
620
|
-
keywords: kws
|
|
621
|
-
});
|
|
622
|
-
kws.forEach((kw) => clusteredKeywords.add(kw.keyword));
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
const clusters = [];
|
|
626
|
-
for (const [name, data] of clusterMap) {
|
|
627
|
-
if (data.keywords.length < minClusterSize) continue;
|
|
628
|
-
const totalClicks = data.keywords.reduce((sum, k) => sum + num(k.clicks), 0);
|
|
629
|
-
const totalImpressions = data.keywords.reduce((sum, k) => sum + num(k.impressions), 0);
|
|
630
|
-
const avgPosition = data.keywords.reduce((sum, k) => sum + num(k.position), 0) / data.keywords.length;
|
|
631
|
-
clusters.push({
|
|
632
|
-
clusterName: name,
|
|
633
|
-
clusterType: data.type,
|
|
634
|
-
keywords: data.keywords,
|
|
635
|
-
totalClicks,
|
|
636
|
-
totalImpressions,
|
|
637
|
-
avgPosition,
|
|
638
|
-
keywordCount: data.keywords.length
|
|
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
|
|
639
641
|
});
|
|
640
642
|
}
|
|
641
|
-
|
|
642
|
-
return {
|
|
643
|
-
clusters,
|
|
644
|
-
unclustered: filtered.filter((kw) => !clusteredKeywords.has(kw.keyword))
|
|
645
|
-
};
|
|
643
|
+
return sortResults(Array.from(queryMap.values()), "impressions", "desc");
|
|
646
644
|
}
|
|
647
645
|
|
|
648
646
|
//#endregion
|
|
@@ -721,52 +719,6 @@ async function batchInspectUrls(client, siteUrl, urls, options = {}) {
|
|
|
721
719
|
return results;
|
|
722
720
|
}
|
|
723
721
|
|
|
724
|
-
//#endregion
|
|
725
|
-
//#region src/api/sites.ts
|
|
726
|
-
/**
|
|
727
|
-
* Fetches all sites the authenticated user has access to in Google Search Console.
|
|
728
|
-
*/
|
|
729
|
-
async function fetchSites(client) {
|
|
730
|
-
return client.sites.list().then((res) => res?.siteEntry || []);
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* Fetches sitemaps for a site.
|
|
734
|
-
*/
|
|
735
|
-
async function fetchSitemaps(client, siteUrl) {
|
|
736
|
-
return client.sitemaps.list(siteUrl).then((res) => res.sitemap || []);
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Gets details for a specific sitemap.
|
|
740
|
-
*/
|
|
741
|
-
async function fetchSitemap(client, siteUrl, feedpath) {
|
|
742
|
-
return client.sitemaps.get(siteUrl, feedpath);
|
|
743
|
-
}
|
|
744
|
-
/**
|
|
745
|
-
* Submits a sitemap to Google Search Console.
|
|
746
|
-
*/
|
|
747
|
-
async function submitSitemap(client, siteUrl, feedpath) {
|
|
748
|
-
return client.sitemaps.submit(siteUrl, feedpath);
|
|
749
|
-
}
|
|
750
|
-
/**
|
|
751
|
-
* Deletes a sitemap from Google Search Console.
|
|
752
|
-
*/
|
|
753
|
-
async function deleteSitemap(client, siteUrl, feedpath) {
|
|
754
|
-
return client.sitemaps.delete(siteUrl, feedpath);
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Fetches all verified sites with their sitemaps from Google Search Console.
|
|
758
|
-
*/
|
|
759
|
-
async function fetchSitesWithSitemaps(client) {
|
|
760
|
-
const sites = (await client.sites.list().then((res) => res.siteEntry || [])).filter((s) => !!s.siteUrl && s.permissionLevel !== "siteUnverifiedUser");
|
|
761
|
-
return Promise.all(sites.map(async (site) => {
|
|
762
|
-
const sitemaps = site.permissionLevel === "siteOwner" ? await client.sitemaps.list(site.siteUrl).then((res) => res.sitemap || []) : [];
|
|
763
|
-
return {
|
|
764
|
-
...site,
|
|
765
|
-
sitemaps
|
|
766
|
-
};
|
|
767
|
-
}));
|
|
768
|
-
}
|
|
769
|
-
|
|
770
722
|
//#endregion
|
|
771
723
|
//#region src/utils/dayjs.ts
|
|
772
724
|
_dayjs.extend(utc);
|
|
@@ -774,6 +726,12 @@ _dayjs.extend(timezone);
|
|
|
774
726
|
function dayjs(date$1) {
|
|
775
727
|
return _dayjs(date$1);
|
|
776
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
|
+
}
|
|
777
735
|
|
|
778
736
|
//#endregion
|
|
779
737
|
//#region src/utils/format.ts
|
|
@@ -798,59 +756,6 @@ function percentDifference(a, b) {
|
|
|
798
756
|
if (!b || !a) return 0;
|
|
799
757
|
return (a - b) / ((a + b) / 2) * 100;
|
|
800
758
|
}
|
|
801
|
-
/**
|
|
802
|
-
* Resolves a period string or object into start/end dates for current and previous periods.
|
|
803
|
-
* @param periodRange - Period string (e.g., '30d', '3mo', 'all') or Period object with start/end dates
|
|
804
|
-
* @returns Resolved period ranges with timestamps and formatted dates for both current and previous periods
|
|
805
|
-
*/
|
|
806
|
-
function userPeriodRange(periodRange = "30d") {
|
|
807
|
-
let startPeriod;
|
|
808
|
-
let endPeriod;
|
|
809
|
-
let startPrevPeriod;
|
|
810
|
-
let endPrevPeriod;
|
|
811
|
-
if (typeof periodRange === "string") {
|
|
812
|
-
endPeriod = dayjs();
|
|
813
|
-
if (periodRange === "all") {
|
|
814
|
-
startPeriod = dayjs().subtract(100, "year");
|
|
815
|
-
startPrevPeriod = dayjs().subtract(200, "year");
|
|
816
|
-
endPrevPeriod = dayjs().subtract(100, "year").subtract(1, "day");
|
|
817
|
-
} else if (periodRange === "max") {
|
|
818
|
-
const maxDays = 480;
|
|
819
|
-
startPeriod = endPeriod.clone().subtract(maxDays, "day");
|
|
820
|
-
startPrevPeriod = endPeriod.clone().subtract(maxDays * 2, "day");
|
|
821
|
-
endPrevPeriod = endPeriod.clone().subtract(maxDays + 1, "day");
|
|
822
|
-
} else {
|
|
823
|
-
const periodDays = periodRange.includes("d") ? Number.parseInt(periodRange.replace("d", "")) : Number.parseInt(periodRange.replace("mo", "")) * 30;
|
|
824
|
-
startPeriod = endPeriod.clone().subtract(periodDays, "day");
|
|
825
|
-
startPrevPeriod = endPeriod.clone().subtract(periodDays * 2, "day");
|
|
826
|
-
endPrevPeriod = endPeriod.clone().subtract(periodDays + 1, "day");
|
|
827
|
-
}
|
|
828
|
-
} else {
|
|
829
|
-
startPeriod = dayjs(periodRange.start);
|
|
830
|
-
endPeriod = dayjs(periodRange.end);
|
|
831
|
-
const dayDiff = endPeriod.diff(startPeriod, "day");
|
|
832
|
-
startPrevPeriod = dayjs(periodRange.start).subtract(dayDiff, "day");
|
|
833
|
-
endPrevPeriod = dayjs(periodRange.end).subtract(dayDiff, "day");
|
|
834
|
-
}
|
|
835
|
-
return {
|
|
836
|
-
period: {
|
|
837
|
-
startTimestamp: startPeriod.valueOf(),
|
|
838
|
-
start: startPeriod.toDate(),
|
|
839
|
-
startDateTime: startPeriod.format("YYYY-MM-DD HH:mm:ss"),
|
|
840
|
-
startDate: startPeriod.format("YYYY-MM-DD"),
|
|
841
|
-
end: endPeriod.toDate(),
|
|
842
|
-
endDate: endPeriod.format("YYYY-MM-DD"),
|
|
843
|
-
endTimestamp: endPeriod.valueOf(),
|
|
844
|
-
endDateTime: endPeriod.format("YYYY-MM-DD HH:mm:ss")
|
|
845
|
-
},
|
|
846
|
-
prevPeriod: {
|
|
847
|
-
start: startPrevPeriod.toDate(),
|
|
848
|
-
startDate: startPrevPeriod.format("YYYY-MM-DD"),
|
|
849
|
-
end: endPrevPeriod.toDate(),
|
|
850
|
-
endDate: endPrevPeriod.format("YYYY-MM-DD")
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
759
|
|
|
855
760
|
//#endregion
|
|
856
761
|
//#region src/api/search-analytics/query.ts
|
|
@@ -3209,6 +3114,52 @@ async function collectStream(gen) {
|
|
|
3209
3114
|
return all;
|
|
3210
3115
|
}
|
|
3211
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
|
+
*/
|
|
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
|
+
}));
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3212
3163
|
//#endregion
|
|
3213
3164
|
//#region src/core/client.ts
|
|
3214
3165
|
const GSC_API = "https://searchconsole.googleapis.com";
|
|
@@ -3552,6 +3503,7 @@ function resolveToBody(state) {
|
|
|
3552
3503
|
endDate
|
|
3553
3504
|
};
|
|
3554
3505
|
if (state.rowLimit) body.rowLimit = state.rowLimit;
|
|
3506
|
+
if (state.startRow) body.startRow = state.startRow;
|
|
3555
3507
|
const filterGroups = resolveFilters(nonDateFilters);
|
|
3556
3508
|
if (filterGroups.length > 0) body.dimensionFilterGroups = filterGroups;
|
|
3557
3509
|
return body;
|
|
@@ -3616,6 +3568,12 @@ function createBuilder(state) {
|
|
|
3616
3568
|
rowLimit: n
|
|
3617
3569
|
});
|
|
3618
3570
|
},
|
|
3571
|
+
offset(n) {
|
|
3572
|
+
return createBuilder({
|
|
3573
|
+
...state,
|
|
3574
|
+
startRow: n
|
|
3575
|
+
});
|
|
3576
|
+
},
|
|
3619
3577
|
async execute(client) {
|
|
3620
3578
|
const body = resolveToBody(state);
|
|
3621
3579
|
return transformResponse(await client.searchAnalytics.query(state.siteUrl, body), state.dimensions);
|
|
@@ -3645,6 +3603,15 @@ const country = createColumn("country");
|
|
|
3645
3603
|
const searchAppearance = createColumn("searchAppearance");
|
|
3646
3604
|
const date = createColumn("date");
|
|
3647
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
|
+
|
|
3648
3615
|
//#endregion
|
|
3649
3616
|
//#region src/query/operators.ts
|
|
3650
3617
|
function eq(column, value) {
|
|
@@ -3811,13 +3778,4 @@ function between(column, start, end) {
|
|
|
3811
3778
|
}
|
|
3812
3779
|
|
|
3813
3780
|
//#endregion
|
|
3814
|
-
|
|
3815
|
-
const Device = {
|
|
3816
|
-
MOBILE: "MOBILE",
|
|
3817
|
-
DESKTOP: "DESKTOP",
|
|
3818
|
-
TABLET: "TABLET"
|
|
3819
|
-
};
|
|
3820
|
-
const Country = Object.fromEntries(countries_default.map((c) => [c["alpha-3"], c["alpha-3"].toLowerCase()]));
|
|
3821
|
-
|
|
3822
|
-
//#endregion
|
|
3823
|
-
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, date, dayjs, 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, userPeriodRange, withDataType, withFinalData, withFreshData, withPropertyAggregation, withSearchAppearance };
|
|
3781
|
+
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, 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, withDataType, withFinalData, withFreshData, withPropertyAggregation, withSearchAppearance };
|