@pagebridge/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cannibalization-analyzer.d.ts +38 -0
- package/dist/cannibalization-analyzer.d.ts.map +1 -0
- package/dist/cannibalization-analyzer.js +109 -0
- package/dist/ctr-anomaly-analyzer.d.ts +46 -0
- package/dist/ctr-anomaly-analyzer.d.ts.map +1 -0
- package/dist/ctr-anomaly-analyzer.js +118 -0
- package/dist/daily-metrics-collector.d.ts +12 -0
- package/dist/daily-metrics-collector.d.ts.map +1 -0
- package/dist/daily-metrics-collector.js +47 -0
- package/dist/decay-detector.d.ts.map +1 -1
- package/dist/decay-detector.js +1 -13
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/insight-generator.d.ts +100 -0
- package/dist/insight-generator.d.ts.map +1 -0
- package/dist/insight-generator.js +286 -0
- package/dist/insight-writer.d.ts +26 -0
- package/dist/insight-writer.d.ts.map +1 -0
- package/dist/insight-writer.js +87 -0
- package/dist/publishing-impact-analyzer.d.ts +23 -0
- package/dist/publishing-impact-analyzer.d.ts.map +1 -0
- package/dist/publishing-impact-analyzer.js +79 -0
- package/dist/quick-win-analyzer.d.ts.map +1 -1
- package/dist/quick-win-analyzer.js +1 -8
- package/dist/site-insight-analyzer.d.ts +45 -0
- package/dist/site-insight-analyzer.d.ts.map +1 -0
- package/dist/site-insight-analyzer.js +144 -0
- package/dist/sync-engine.d.ts +34 -3
- package/dist/sync-engine.d.ts.map +1 -1
- package/dist/sync-engine.js +54 -10
- package/dist/utils/date-utils.d.ts +4 -0
- package/dist/utils/date-utils.d.ts.map +1 -0
- package/dist/utils/date-utils.js +13 -0
- package/dist/utils/sanity-key.d.ts +6 -0
- package/dist/utils/sanity-key.d.ts.map +1 -0
- package/dist/utils/sanity-key.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { searchAnalytics, queryAnalytics } from "@pagebridge/db";
|
|
2
|
+
import { and, eq, gte, lte, sql } from "drizzle-orm";
|
|
3
|
+
import { daysAgo, formatDate } from "./utils/date-utils.js";
|
|
4
|
+
export class SiteInsightAnalyzer {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
async analyze(siteId, allPages) {
|
|
10
|
+
const [topPerformers, zeroClickPages, orphanPages, newKeywords] = await Promise.all([
|
|
11
|
+
this.getTopPerformers(siteId),
|
|
12
|
+
this.getZeroClickPages(siteId),
|
|
13
|
+
this.getOrphanPages(siteId, allPages),
|
|
14
|
+
this.getNewKeywordOpportunities(siteId),
|
|
15
|
+
]);
|
|
16
|
+
return {
|
|
17
|
+
topPerformers,
|
|
18
|
+
zeroClickPages,
|
|
19
|
+
orphanPages,
|
|
20
|
+
newKeywordOpportunities: newKeywords,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async getTopPerformers(siteId) {
|
|
24
|
+
const startDate = daysAgo(28);
|
|
25
|
+
const endDate = daysAgo(3);
|
|
26
|
+
const results = await this.db
|
|
27
|
+
.select({
|
|
28
|
+
page: searchAnalytics.page,
|
|
29
|
+
totalClicks: sql `sum(${searchAnalytics.clicks})`,
|
|
30
|
+
totalImpressions: sql `sum(${searchAnalytics.impressions})`,
|
|
31
|
+
avgPosition: sql `avg(${searchAnalytics.position})`,
|
|
32
|
+
})
|
|
33
|
+
.from(searchAnalytics)
|
|
34
|
+
.where(and(eq(searchAnalytics.siteId, siteId), gte(searchAnalytics.date, formatDate(startDate)), lte(searchAnalytics.date, formatDate(endDate))))
|
|
35
|
+
.groupBy(searchAnalytics.page);
|
|
36
|
+
return results
|
|
37
|
+
.map((r) => ({
|
|
38
|
+
page: r.page,
|
|
39
|
+
clicks: Number(r.totalClicks) || 0,
|
|
40
|
+
impressions: Number(r.totalImpressions) || 0,
|
|
41
|
+
position: Number(r.avgPosition) || 0,
|
|
42
|
+
}))
|
|
43
|
+
.sort((a, b) => b.clicks - a.clicks)
|
|
44
|
+
.slice(0, 20);
|
|
45
|
+
}
|
|
46
|
+
async getZeroClickPages(siteId) {
|
|
47
|
+
const startDate = daysAgo(28);
|
|
48
|
+
const endDate = daysAgo(3);
|
|
49
|
+
const results = await this.db
|
|
50
|
+
.select({
|
|
51
|
+
page: searchAnalytics.page,
|
|
52
|
+
totalClicks: sql `sum(${searchAnalytics.clicks})`,
|
|
53
|
+
totalImpressions: sql `sum(${searchAnalytics.impressions})`,
|
|
54
|
+
avgPosition: sql `avg(${searchAnalytics.position})`,
|
|
55
|
+
})
|
|
56
|
+
.from(searchAnalytics)
|
|
57
|
+
.where(and(eq(searchAnalytics.siteId, siteId), gte(searchAnalytics.date, formatDate(startDate)), lte(searchAnalytics.date, formatDate(endDate))))
|
|
58
|
+
.groupBy(searchAnalytics.page);
|
|
59
|
+
return results
|
|
60
|
+
.filter((r) => {
|
|
61
|
+
const impressions = Number(r.totalImpressions) || 0;
|
|
62
|
+
const clicks = Number(r.totalClicks) || 0;
|
|
63
|
+
return impressions >= 100 && clicks <= 2;
|
|
64
|
+
})
|
|
65
|
+
.map((r) => ({
|
|
66
|
+
page: r.page,
|
|
67
|
+
impressions: Number(r.totalImpressions) || 0,
|
|
68
|
+
clicks: Number(r.totalClicks) || 0,
|
|
69
|
+
position: Number(r.avgPosition) || 0,
|
|
70
|
+
}))
|
|
71
|
+
.sort((a, b) => b.impressions - a.impressions);
|
|
72
|
+
}
|
|
73
|
+
async getOrphanPages(siteId, allPages) {
|
|
74
|
+
const startDate = daysAgo(28);
|
|
75
|
+
const endDate = daysAgo(3);
|
|
76
|
+
// Get pages that have had impressions in last 28 days
|
|
77
|
+
const activeResults = await this.db
|
|
78
|
+
.select({
|
|
79
|
+
page: searchAnalytics.page,
|
|
80
|
+
})
|
|
81
|
+
.from(searchAnalytics)
|
|
82
|
+
.where(and(eq(searchAnalytics.siteId, siteId), gte(searchAnalytics.date, formatDate(startDate)), lte(searchAnalytics.date, formatDate(endDate))))
|
|
83
|
+
.groupBy(searchAnalytics.page);
|
|
84
|
+
const activePages = new Set(activeResults.map((r) => r.page));
|
|
85
|
+
const orphanPageUrls = allPages.filter((page) => !activePages.has(page));
|
|
86
|
+
if (orphanPageUrls.length === 0)
|
|
87
|
+
return [];
|
|
88
|
+
// Query the last impression date for each orphan page
|
|
89
|
+
const lastImpressionResults = await this.db
|
|
90
|
+
.select({
|
|
91
|
+
page: searchAnalytics.page,
|
|
92
|
+
lastDate: sql `max(${searchAnalytics.date})`,
|
|
93
|
+
})
|
|
94
|
+
.from(searchAnalytics)
|
|
95
|
+
.where(and(eq(searchAnalytics.siteId, siteId), sql `${searchAnalytics.page} IN (${sql.join(orphanPageUrls.map((u) => sql `${u}`), sql `, `)})`))
|
|
96
|
+
.groupBy(searchAnalytics.page);
|
|
97
|
+
const lastImpressionMap = new Map();
|
|
98
|
+
for (const row of lastImpressionResults) {
|
|
99
|
+
if (row.lastDate)
|
|
100
|
+
lastImpressionMap.set(row.page, row.lastDate);
|
|
101
|
+
}
|
|
102
|
+
return orphanPageUrls.map((page) => ({
|
|
103
|
+
page,
|
|
104
|
+
lastImpression: lastImpressionMap.get(page),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
async getNewKeywordOpportunities(siteId) {
|
|
108
|
+
const recentStart = daysAgo(7);
|
|
109
|
+
const recentEnd = daysAgo(0);
|
|
110
|
+
const historicStart = daysAgo(90);
|
|
111
|
+
const historicEnd = daysAgo(14);
|
|
112
|
+
// Get queries from last 7 days
|
|
113
|
+
const recentQueries = await this.db
|
|
114
|
+
.select({
|
|
115
|
+
query: queryAnalytics.query,
|
|
116
|
+
page: queryAnalytics.page,
|
|
117
|
+
totalImpressions: sql `sum(${queryAnalytics.impressions})`,
|
|
118
|
+
avgPosition: sql `avg(${queryAnalytics.position})`,
|
|
119
|
+
})
|
|
120
|
+
.from(queryAnalytics)
|
|
121
|
+
.where(and(eq(queryAnalytics.siteId, siteId), gte(queryAnalytics.date, formatDate(recentStart)), lte(queryAnalytics.date, formatDate(recentEnd))))
|
|
122
|
+
.groupBy(queryAnalytics.query, queryAnalytics.page);
|
|
123
|
+
// Get queries seen in days 14-90
|
|
124
|
+
const historicQueryResults = await this.db
|
|
125
|
+
.select({
|
|
126
|
+
query: queryAnalytics.query,
|
|
127
|
+
})
|
|
128
|
+
.from(queryAnalytics)
|
|
129
|
+
.where(and(eq(queryAnalytics.siteId, siteId), gte(queryAnalytics.date, formatDate(historicStart)), lte(queryAnalytics.date, formatDate(historicEnd))))
|
|
130
|
+
.groupBy(queryAnalytics.query);
|
|
131
|
+
const historicQueries = new Set(historicQueryResults.map((r) => r.query));
|
|
132
|
+
return recentQueries
|
|
133
|
+
.filter((r) => !historicQueries.has(r.query) &&
|
|
134
|
+
(Number(r.totalImpressions) || 0) >= 10)
|
|
135
|
+
.map((r) => ({
|
|
136
|
+
query: r.query,
|
|
137
|
+
page: r.page,
|
|
138
|
+
impressions: Number(r.totalImpressions) || 0,
|
|
139
|
+
position: Number(r.avgPosition) || 0,
|
|
140
|
+
}))
|
|
141
|
+
.sort((a, b) => b.impressions - a.impressions)
|
|
142
|
+
.slice(0, 50);
|
|
143
|
+
}
|
|
144
|
+
}
|
package/dist/sync-engine.d.ts
CHANGED
|
@@ -2,6 +2,39 @@ import type { DrizzleClient } from "@pagebridge/db";
|
|
|
2
2
|
import type { SanityClient } from "@sanity/client";
|
|
3
3
|
import type { GSCClient, IndexStatusResult } from "./gsc-client.js";
|
|
4
4
|
import type { QuickWinQuery } from "./quick-win-analyzer.js";
|
|
5
|
+
import type { CtrAnomaly } from "./ctr-anomaly-analyzer.js";
|
|
6
|
+
export interface PublishingImpact {
|
|
7
|
+
lastEditedAt: string;
|
|
8
|
+
daysSinceEdit: number;
|
|
9
|
+
positionBefore: number;
|
|
10
|
+
positionAfter: number;
|
|
11
|
+
positionDelta: number;
|
|
12
|
+
clicksBefore: number;
|
|
13
|
+
clicksAfter: number;
|
|
14
|
+
impressionsBefore: number;
|
|
15
|
+
impressionsAfter: number;
|
|
16
|
+
ctrBefore: number;
|
|
17
|
+
ctrAfter: number;
|
|
18
|
+
}
|
|
19
|
+
export interface CannibalizationTarget {
|
|
20
|
+
competingPage: string;
|
|
21
|
+
competingDocumentId: string;
|
|
22
|
+
sharedQueries: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface DailyMetricPoint {
|
|
25
|
+
date: string;
|
|
26
|
+
clicks: number;
|
|
27
|
+
impressions: number;
|
|
28
|
+
position: number;
|
|
29
|
+
}
|
|
30
|
+
export interface SnapshotInsights {
|
|
31
|
+
quickWins?: Map<string, QuickWinQuery[]>;
|
|
32
|
+
ctrAnomalies?: Map<string, CtrAnomaly>;
|
|
33
|
+
dailyMetrics?: Map<string, DailyMetricPoint[]>;
|
|
34
|
+
publishingImpact?: Map<string, PublishingImpact>;
|
|
35
|
+
cannibalizationTargets?: Map<string, CannibalizationTarget[]>;
|
|
36
|
+
decayPages?: Set<string>;
|
|
37
|
+
}
|
|
5
38
|
export interface SyncOptions {
|
|
6
39
|
siteUrl: string;
|
|
7
40
|
startDate?: Date;
|
|
@@ -34,9 +67,7 @@ export declare class SyncEngine {
|
|
|
34
67
|
writeSnapshots(siteId: string, matches: {
|
|
35
68
|
gscUrl: string;
|
|
36
69
|
sanityId: string | undefined;
|
|
37
|
-
}[], siteUrl?: string, insights?:
|
|
38
|
-
quickWins?: Map<string, QuickWinQuery[]>;
|
|
39
|
-
}, onProgress?: (message: string) => void): Promise<void>;
|
|
70
|
+
}[], siteUrl?: string, insights?: SnapshotInsights, onProgress?: (message: string) => void): Promise<void>;
|
|
40
71
|
syncIndexStatus(siteUrl: string, pages: string[]): Promise<IndexStatusSyncResult>;
|
|
41
72
|
getIndexStatus(siteUrl: string, page: string): Promise<IndexStatusResult | null>;
|
|
42
73
|
private getAggregatedMetrics;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-engine.d.ts","sourceRoot":"","sources":["../src/sync-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAQpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"sync-engine.d.ts","sourceRoot":"","sources":["../src/sync-engine.ts"],"names":[],"mappings":"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"}
|
package/dist/sync-engine.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { searchAnalytics, queryAnalytics, syncLog, pageIndexStatus, } from "@pagebridge/db";
|
|
2
2
|
import { and, eq, gte, lte, sql } from "drizzle-orm";
|
|
3
|
+
import { CtrAnomalyAnalyzer } from "./ctr-anomaly-analyzer.js";
|
|
4
|
+
import { daysAgo, formatDate } from "./utils/date-utils.js";
|
|
5
|
+
import { sanityKey } from "./utils/sanity-key.js";
|
|
3
6
|
export class SyncEngine {
|
|
4
7
|
gsc;
|
|
5
8
|
db;
|
|
@@ -187,9 +190,46 @@ export class SyncEngine {
|
|
|
187
190
|
if (!metrics)
|
|
188
191
|
continue;
|
|
189
192
|
const indexStatusData = indexStatusMap.get(match.gscUrl);
|
|
190
|
-
const
|
|
193
|
+
const isLast28 = period === "last28";
|
|
194
|
+
const quickWinQueries = isLast28
|
|
191
195
|
? (insights?.quickWins?.get(match.gscUrl) ?? [])
|
|
192
196
|
: [];
|
|
197
|
+
const ctrAnomaly = isLast28
|
|
198
|
+
? insights?.ctrAnomalies?.get(match.gscUrl)
|
|
199
|
+
: undefined;
|
|
200
|
+
const dailyClicks = isLast28
|
|
201
|
+
? insights?.dailyMetrics?.get(match.gscUrl)
|
|
202
|
+
: undefined;
|
|
203
|
+
const publishingImpact = isLast28
|
|
204
|
+
? insights?.publishingImpact?.get(match.gscUrl)
|
|
205
|
+
: undefined;
|
|
206
|
+
const cannibalizationTargets = isLast28
|
|
207
|
+
? insights?.cannibalizationTargets?.get(match.gscUrl)
|
|
208
|
+
: undefined;
|
|
209
|
+
// Build alerts from all insight sources
|
|
210
|
+
const hasQuickWins = quickWinQueries.length > 0;
|
|
211
|
+
const hasDecay = insights?.decayPages?.has(match.gscUrl) ?? false;
|
|
212
|
+
const hasCannibalization = (cannibalizationTargets?.length ?? 0) > 0;
|
|
213
|
+
const alerts = isLast28
|
|
214
|
+
? CtrAnomalyAnalyzer.buildAlerts(ctrAnomaly, hasQuickWins, hasDecay, hasCannibalization)
|
|
215
|
+
: [];
|
|
216
|
+
// Add _key to all array items for Sanity
|
|
217
|
+
const keyedQuickWins = quickWinQueries.map((q) => ({
|
|
218
|
+
_key: sanityKey(`qw:${q.query}`),
|
|
219
|
+
...q,
|
|
220
|
+
}));
|
|
221
|
+
const keyedAlerts = alerts.map((a) => ({
|
|
222
|
+
_key: sanityKey(`al:${a.type}:${a.severity}`),
|
|
223
|
+
...a,
|
|
224
|
+
}));
|
|
225
|
+
const keyedDailyClicks = dailyClicks?.map((d) => ({
|
|
226
|
+
_key: sanityKey(`dc:${d.date}`),
|
|
227
|
+
...d,
|
|
228
|
+
}));
|
|
229
|
+
const keyedCannibalization = cannibalizationTargets?.map((t) => ({
|
|
230
|
+
_key: sanityKey(`ct:${t.competingPage}`),
|
|
231
|
+
...t,
|
|
232
|
+
}));
|
|
193
233
|
const snapshotData = {
|
|
194
234
|
_type: "gscSnapshot",
|
|
195
235
|
site: { _type: "reference", _ref: siteId },
|
|
@@ -204,7 +244,18 @@ export class SyncEngine {
|
|
|
204
244
|
ctr: metrics.ctr,
|
|
205
245
|
position: metrics.position,
|
|
206
246
|
topQueries,
|
|
207
|
-
...(
|
|
247
|
+
...(keyedQuickWins.length > 0
|
|
248
|
+
? { quickWinQueries: keyedQuickWins }
|
|
249
|
+
: {}),
|
|
250
|
+
...(ctrAnomaly ? { ctrAnomaly } : {}),
|
|
251
|
+
...(keyedAlerts.length > 0 ? { alerts: keyedAlerts } : {}),
|
|
252
|
+
...(keyedDailyClicks && keyedDailyClicks.length > 0
|
|
253
|
+
? { dailyClicks: keyedDailyClicks }
|
|
254
|
+
: {}),
|
|
255
|
+
...(publishingImpact ? { publishingImpact } : {}),
|
|
256
|
+
...(keyedCannibalization && keyedCannibalization.length > 0
|
|
257
|
+
? { cannibalizationTargets: keyedCannibalization }
|
|
258
|
+
: {}),
|
|
208
259
|
fetchedAt: new Date().toISOString(),
|
|
209
260
|
indexStatus: indexStatusData
|
|
210
261
|
? {
|
|
@@ -368,6 +419,7 @@ export class SyncEngine {
|
|
|
368
419
|
}
|
|
369
420
|
return Array.from(queryMap.entries())
|
|
370
421
|
.map(([query, data]) => ({
|
|
422
|
+
_key: sanityKey(`tq:${query}`),
|
|
371
423
|
query,
|
|
372
424
|
clicks: data.clicks,
|
|
373
425
|
impressions: data.impressions,
|
|
@@ -377,14 +429,6 @@ export class SyncEngine {
|
|
|
377
429
|
.slice(0, 10);
|
|
378
430
|
}
|
|
379
431
|
}
|
|
380
|
-
function daysAgo(days) {
|
|
381
|
-
const date = new Date();
|
|
382
|
-
date.setDate(date.getDate() - days);
|
|
383
|
-
return date;
|
|
384
|
-
}
|
|
385
|
-
function formatDate(date) {
|
|
386
|
-
return date.toISOString().split("T")[0];
|
|
387
|
-
}
|
|
388
432
|
function delay(ms) {
|
|
389
433
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
390
434
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-utils.d.ts","sourceRoot":"","sources":["../../src/utils/date-utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI1C;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE7C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAI5C"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function daysAgo(days) {
|
|
2
|
+
const date = new Date();
|
|
3
|
+
date.setDate(date.getDate() - days);
|
|
4
|
+
return date;
|
|
5
|
+
}
|
|
6
|
+
export function formatDate(date) {
|
|
7
|
+
return date.toISOString().split("T")[0];
|
|
8
|
+
}
|
|
9
|
+
export function daysSince(date) {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const diff = now.getTime() - date.getTime();
|
|
12
|
+
return Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanity-key.d.ts","sourceRoot":"","sources":["../../src/utils/sanity-key.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Generates a deterministic Sanity `_key` from a string seed.
|
|
4
|
+
* Uses a short hash so keys stay compact but unique within an array.
|
|
5
|
+
*/
|
|
6
|
+
export function sanityKey(seed) {
|
|
7
|
+
return createHash("sha256").update(seed).digest("hex").slice(0, 12);
|
|
8
|
+
}
|