@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.
Files changed (38) hide show
  1. package/dist/cannibalization-analyzer.d.ts +38 -0
  2. package/dist/cannibalization-analyzer.d.ts.map +1 -0
  3. package/dist/cannibalization-analyzer.js +109 -0
  4. package/dist/ctr-anomaly-analyzer.d.ts +46 -0
  5. package/dist/ctr-anomaly-analyzer.d.ts.map +1 -0
  6. package/dist/ctr-anomaly-analyzer.js +118 -0
  7. package/dist/daily-metrics-collector.d.ts +12 -0
  8. package/dist/daily-metrics-collector.d.ts.map +1 -0
  9. package/dist/daily-metrics-collector.js +47 -0
  10. package/dist/decay-detector.d.ts.map +1 -1
  11. package/dist/decay-detector.js +1 -13
  12. package/dist/index.d.ts +8 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +7 -0
  15. package/dist/insight-generator.d.ts +100 -0
  16. package/dist/insight-generator.d.ts.map +1 -0
  17. package/dist/insight-generator.js +286 -0
  18. package/dist/insight-writer.d.ts +26 -0
  19. package/dist/insight-writer.d.ts.map +1 -0
  20. package/dist/insight-writer.js +87 -0
  21. package/dist/publishing-impact-analyzer.d.ts +23 -0
  22. package/dist/publishing-impact-analyzer.d.ts.map +1 -0
  23. package/dist/publishing-impact-analyzer.js +79 -0
  24. package/dist/quick-win-analyzer.d.ts.map +1 -1
  25. package/dist/quick-win-analyzer.js +1 -8
  26. package/dist/site-insight-analyzer.d.ts +45 -0
  27. package/dist/site-insight-analyzer.d.ts.map +1 -0
  28. package/dist/site-insight-analyzer.js +144 -0
  29. package/dist/sync-engine.d.ts +34 -3
  30. package/dist/sync-engine.d.ts.map +1 -1
  31. package/dist/sync-engine.js +54 -10
  32. package/dist/utils/date-utils.d.ts +4 -0
  33. package/dist/utils/date-utils.d.ts.map +1 -0
  34. package/dist/utils/date-utils.js +13 -0
  35. package/dist/utils/sanity-key.d.ts +6 -0
  36. package/dist/utils/sanity-key.d.ts.map +1 -0
  37. package/dist/utils/sanity-key.js +8 -0
  38. 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
+ }
@@ -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;AAE7D,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,UAAU,CAAC,EAAE,CAAC,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,EAAE,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,SAAS,CAAC;IACf,EAAE,EAAE,aAAa,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,GAAG,CAAY;IACvB,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,MAAM,CAAe;gBAEjB,OAAO,EAAE,iBAAiB;IAMhC,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAgK/C,cAAc,CAClB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,EAAE,EAC3D,OAAO,CAAC,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE;QACT,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;KAC1C,EACD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GACrC,OAAO,CAAC,IAAI,CAAC;IAqIV,eAAe,CACnB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,qBAAqB,CAAC;IA6E3B,cAAc,CAClB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;YAqBtB,oBAAoB;YAiDpB,aAAa;CAyD5B"}
1
+ {"version":3,"file":"sync-engine.d.ts","sourceRoot":"","sources":["../src/sync-engine.ts"],"names":[],"mappings":"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"}
@@ -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 quickWinQueries = period === "last28"
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
- ...(quickWinQueries.length > 0 ? { quickWinQueries } : {}),
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,4 @@
1
+ export declare function daysAgo(days: number): Date;
2
+ export declare function formatDate(date: Date): string;
3
+ export declare function daysSince(date: Date): number;
4
+ //# sourceMappingURL=date-utils.d.ts.map
@@ -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,6 @@
1
+ /**
2
+ * Generates a deterministic Sanity `_key` from a string seed.
3
+ * Uses a short hash so keys stay compact but unique within an array.
4
+ */
5
+ export declare function sanityKey(seed: string): string;
6
+ //# sourceMappingURL=sanity-key.d.ts.map
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagebridge/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Core engine for PageBridge — GSC sync, URL matching, content decay detection, and task generation",
5
5
  "license": "MIT",
6
6
  "private": false,