@ainyc/canonry 5.1.3 → 5.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/assets/agent-workspace/skills/aero/SKILL.md +43 -9
- package/assets/agent-workspace/skills/aero/references/agent-operations.md +231 -0
- package/assets/agent-workspace/skills/aero/references/orchestration.md +11 -5
- package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +93 -0
- package/assets/agent-workspace/skills/aero/references/reporting.md +10 -1
- package/assets/agent-workspace/skills/aero/references/site-health.md +104 -0
- package/assets/agent-workspace/skills/aero/soul.md +5 -0
- package/assets/assets/{AuditHistoryPanel-Dc_pymFg.js → AuditHistoryPanel-D0H5TtxC.js} +1 -1
- package/assets/assets/{BacklinksPage-BkSo48YX.js → BacklinksPage-DRihwG5O.js} +1 -1
- package/assets/assets/{HistoryPage-BbswsdU7.js → HistoryPage-BQ2nEXly.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-BbgtalkM.js → MeasurementPropertyPage-Bp6EURzJ.js} +1 -1
- package/assets/assets/ProjectPage-Cd03Phwx.js +8 -0
- package/assets/assets/{RunRow-2cNW8FHz.js → RunRow-Bo_Adiy0.js} +1 -1
- package/assets/assets/{RunsPage-Cz3lJ6VJ.js → RunsPage-CDkbzjuo.js} +1 -1
- package/assets/assets/{SettingsPage-BQwB0r9k.js → SettingsPage-CF4i_EzR.js} +1 -1
- package/assets/assets/SiteHealthSection-BrzBV7dt.js +4 -0
- package/assets/assets/{TrafficPage-DTic3zhO.js → TrafficPage-BHSz1N4P.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-BKfabnCw.js → TrafficSourceDetailPage-DzHLwhpy.js} +1 -1
- package/assets/assets/{extract-error-message-BzB8j3fC.js → extract-error-message-WHM24Sai.js} +1 -1
- package/assets/assets/{index-DQGEzxrN.js → index-5W4ZZnwg.js} +22 -22
- package/assets/assets/index-DCLmKL1s.css +1 -0
- package/assets/assets/{react-sigma_core.esm.min-rYv0aKKx.js → react-sigma_core.esm.min-BB2Lcq4W.js} +1 -1
- package/assets/assets/{v2-overview-adapter-CWWPfsm-.js → v2-overview-adapter-ebrNjcUG.js} +1 -1
- package/assets/assets/vendor-lucide-D_WBzb76.js +1 -0
- package/assets/index.html +3 -3
- package/dist/{chunk-MMSPU72Z.js → chunk-2U5IW3G5.js} +4 -0
- package/dist/chunk-5VONCLVQ.js +652 -0
- package/dist/chunk-AOJU5LKX.js +8 -0
- package/dist/{chunk-KJM4DRZN.js → chunk-C7PITYL3.js} +5 -3
- package/dist/{chunk-5ZOLPHTO.js → chunk-CW2KPN3L.js} +331 -943
- package/dist/{chunk-AEE5DGTE.js → chunk-IOP36L3C.js} +4 -7
- package/dist/chunk-RB2F4D3Z.js +67 -0
- package/dist/{chunk-EBUX3C5H.js → chunk-YQNKULAD.js} +30 -10
- package/dist/cli.js +100 -5
- package/dist/demo-server-BIPT53EY.js +2125 -0
- package/dist/index.js +6 -4
- package/dist/intelligence-service-SJO5E7WN.js +9 -0
- package/dist/mcp.js +4 -3
- package/package.json +11 -11
- package/assets/assets/ProjectPage-CAcRQp-T.js +0 -8
- package/assets/assets/SiteHealthSection-C0aJ_UhN.js +0 -4
- package/assets/assets/index-CjEprBri.css +0 -1
- package/assets/assets/vendor-lucide-CW2qe0uH.js +0 -1
- package/dist/intelligence-service-25NQCETC.js +0 -7
|
@@ -0,0 +1,2125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_DEMO_TRUSTED_PROXIES
|
|
3
|
+
} from "./chunk-RB2F4D3Z.js";
|
|
4
|
+
import {
|
|
5
|
+
layoutSiteCrawlGraphInput,
|
|
6
|
+
persistSiteCrawlGraphLayout
|
|
7
|
+
} from "./chunk-5VONCLVQ.js";
|
|
8
|
+
import {
|
|
9
|
+
PACKAGE_VERSION
|
|
10
|
+
} from "./chunk-AOJU5LKX.js";
|
|
11
|
+
import {
|
|
12
|
+
HISTORY_WINDOW_RUNS,
|
|
13
|
+
IntelligenceService,
|
|
14
|
+
adsAdGroups,
|
|
15
|
+
adsAds,
|
|
16
|
+
adsCampaigns,
|
|
17
|
+
adsConnections,
|
|
18
|
+
adsInsightsDaily,
|
|
19
|
+
aiReferralEventsHourly,
|
|
20
|
+
aiUserFetchEventsHourly,
|
|
21
|
+
apiKeys,
|
|
22
|
+
apiRoutes,
|
|
23
|
+
assessConversionTrackingIntegrity,
|
|
24
|
+
auditLog,
|
|
25
|
+
backlinkDomains,
|
|
26
|
+
backlinkSummaries,
|
|
27
|
+
bingConnections,
|
|
28
|
+
bingCoverageSnapshots,
|
|
29
|
+
bingKeywordStats,
|
|
30
|
+
bingUrlInspections,
|
|
31
|
+
competitors,
|
|
32
|
+
computeCompetitorOverlap,
|
|
33
|
+
conversionTrackingContracts,
|
|
34
|
+
crawlerEventsHourly,
|
|
35
|
+
createClient,
|
|
36
|
+
determineCitationState,
|
|
37
|
+
discoveryProbes,
|
|
38
|
+
discoverySessions,
|
|
39
|
+
extractRecommendedCompetitors,
|
|
40
|
+
gaAcquisitionDaily,
|
|
41
|
+
gaAiReferrals,
|
|
42
|
+
gaDailyTotals,
|
|
43
|
+
gaLeadEventsDaily,
|
|
44
|
+
gaMeasurementSyncStates,
|
|
45
|
+
gaTrafficSnapshots,
|
|
46
|
+
gaTrafficSummaries,
|
|
47
|
+
gaTrafficWindowSummaries,
|
|
48
|
+
gbpDailyMetrics,
|
|
49
|
+
gbpKeywordImpressions,
|
|
50
|
+
gbpKeywordMonthly,
|
|
51
|
+
gbpLocations,
|
|
52
|
+
gbpPlaceActions,
|
|
53
|
+
googleAdsConnections,
|
|
54
|
+
googleAdsRawSnapshots,
|
|
55
|
+
googleConnections,
|
|
56
|
+
gscCoverageSnapshots,
|
|
57
|
+
gscDailyTotals,
|
|
58
|
+
gscDataWatermarks,
|
|
59
|
+
gscQueryDailyTotals,
|
|
60
|
+
gscSearchData,
|
|
61
|
+
gscUrlInspections,
|
|
62
|
+
gtmConnections,
|
|
63
|
+
gtmRawSnapshots,
|
|
64
|
+
healthSnapshots,
|
|
65
|
+
insights,
|
|
66
|
+
measurementPlanDrafts,
|
|
67
|
+
measurementPlanVersions,
|
|
68
|
+
measurementPlans,
|
|
69
|
+
measurementSegments,
|
|
70
|
+
migrate,
|
|
71
|
+
projects,
|
|
72
|
+
queries,
|
|
73
|
+
querySnapshots,
|
|
74
|
+
researchRunQueries,
|
|
75
|
+
researchRuns,
|
|
76
|
+
runs,
|
|
77
|
+
simpleMeasurementDefinitions,
|
|
78
|
+
siteAuditPages,
|
|
79
|
+
siteAuditSnapshots,
|
|
80
|
+
siteCrawlAttempts,
|
|
81
|
+
siteCrawlEdges,
|
|
82
|
+
siteCrawlFindings,
|
|
83
|
+
siteCrawlPages,
|
|
84
|
+
siteCrawlSnapshots,
|
|
85
|
+
trafficSources
|
|
86
|
+
} from "./chunk-YQNKULAD.js";
|
|
87
|
+
import {
|
|
88
|
+
RunKinds,
|
|
89
|
+
SiteCrawlFetchStates,
|
|
90
|
+
SiteCrawlIndexabilityReasons,
|
|
91
|
+
SiteCrawlIndexabilityStates,
|
|
92
|
+
buildMeasurementExecutionIdentity,
|
|
93
|
+
buildMeasurementRunManifestV1,
|
|
94
|
+
buildSimpleMeasurementDefinition,
|
|
95
|
+
canonicalMeasurementExecutionIdentityJson,
|
|
96
|
+
canonicalMeasurementPlanV2Json,
|
|
97
|
+
canonicalSimpleMeasurementDefinitionJson,
|
|
98
|
+
deriveSiteHealthState,
|
|
99
|
+
determineAnswerMentioned,
|
|
100
|
+
effectiveBrandNames,
|
|
101
|
+
effectiveDomains,
|
|
102
|
+
factorStatusFromScore,
|
|
103
|
+
measurementPlanV2ChecksumJson,
|
|
104
|
+
measurementPlanV2Schema,
|
|
105
|
+
placementLinkDecision,
|
|
106
|
+
siteAuditPageFactorSchema
|
|
107
|
+
} from "./chunk-2U5IW3G5.js";
|
|
108
|
+
|
|
109
|
+
// src/demo-server.ts
|
|
110
|
+
import { dirname, join as join2 } from "path";
|
|
111
|
+
import { fileURLToPath } from "url";
|
|
112
|
+
|
|
113
|
+
// src/demo/http.ts
|
|
114
|
+
import { createHash, randomUUID } from "crypto";
|
|
115
|
+
import { existsSync, readFileSync } from "fs";
|
|
116
|
+
import { join } from "path";
|
|
117
|
+
import { desc, eq } from "drizzle-orm";
|
|
118
|
+
import Fastify from "fastify";
|
|
119
|
+
import fastifyStatic from "@fastify/static";
|
|
120
|
+
import rateLimit from "@fastify/rate-limit";
|
|
121
|
+
|
|
122
|
+
// src/demo/access.ts
|
|
123
|
+
var DEMO_READ_ROUTES = /* @__PURE__ */ new Set([
|
|
124
|
+
"/api/v1/auth/session",
|
|
125
|
+
"/api/v1/backlinks/releases",
|
|
126
|
+
"/api/v1/backlinks/status",
|
|
127
|
+
"/api/v1/backlinks/syncs",
|
|
128
|
+
"/api/v1/backlinks/syncs/latest",
|
|
129
|
+
"/api/v1/demo",
|
|
130
|
+
"/api/v1/history",
|
|
131
|
+
"/api/v1/keys/self",
|
|
132
|
+
"/api/v1/notifications/events",
|
|
133
|
+
"/api/v1/projects",
|
|
134
|
+
"/api/v1/projects/:name",
|
|
135
|
+
"/api/v1/projects/:name/ads/campaigns",
|
|
136
|
+
"/api/v1/projects/:name/ads/delivery-diagnostics",
|
|
137
|
+
"/api/v1/projects/:name/ads/insights",
|
|
138
|
+
"/api/v1/projects/:name/ads/operations",
|
|
139
|
+
"/api/v1/projects/:name/ads/operations/:operationKey",
|
|
140
|
+
"/api/v1/projects/:name/ads/status",
|
|
141
|
+
"/api/v1/projects/:name/ads/summary",
|
|
142
|
+
"/api/v1/projects/:name/analytics/competitors",
|
|
143
|
+
"/api/v1/projects/:name/analytics/gaps",
|
|
144
|
+
"/api/v1/projects/:name/analytics/metrics",
|
|
145
|
+
"/api/v1/projects/:name/analytics/sources",
|
|
146
|
+
"/api/v1/projects/:name/backlinks/domains",
|
|
147
|
+
"/api/v1/projects/:name/backlinks/history",
|
|
148
|
+
"/api/v1/projects/:name/backlinks/sources",
|
|
149
|
+
"/api/v1/projects/:name/backlinks/summary",
|
|
150
|
+
"/api/v1/projects/:name/bing/coverage",
|
|
151
|
+
"/api/v1/projects/:name/bing/coverage/history",
|
|
152
|
+
"/api/v1/projects/:name/bing/inspections",
|
|
153
|
+
"/api/v1/projects/:name/bing/performance",
|
|
154
|
+
"/api/v1/projects/:name/bing/status",
|
|
155
|
+
"/api/v1/projects/:name/citations/visibility",
|
|
156
|
+
"/api/v1/projects/:name/competitors",
|
|
157
|
+
"/api/v1/projects/:name/content/dismissals",
|
|
158
|
+
"/api/v1/projects/:name/content/domain-classifications",
|
|
159
|
+
"/api/v1/projects/:name/content/gaps",
|
|
160
|
+
"/api/v1/projects/:name/content/recommendations/:targetRef/analysis",
|
|
161
|
+
"/api/v1/projects/:name/content/recommendations/:targetRef/brief",
|
|
162
|
+
"/api/v1/projects/:name/content/sources",
|
|
163
|
+
"/api/v1/projects/:name/content/targets",
|
|
164
|
+
"/api/v1/projects/:name/conversion-tracking/contracts",
|
|
165
|
+
"/api/v1/projects/:name/conversion-tracking/contracts/:contractId",
|
|
166
|
+
"/api/v1/projects/:name/conversion-tracking/contracts/:contractId/integrity",
|
|
167
|
+
"/api/v1/projects/:name/conversion-tracking/options",
|
|
168
|
+
"/api/v1/projects/:name/discover/sessions",
|
|
169
|
+
"/api/v1/projects/:name/discover/sessions/:id",
|
|
170
|
+
"/api/v1/projects/:name/export",
|
|
171
|
+
"/api/v1/projects/:name/ga/ai-referral-daily",
|
|
172
|
+
"/api/v1/projects/:name/ga/ai-referral-history",
|
|
173
|
+
"/api/v1/projects/:name/ga/attribution-trend",
|
|
174
|
+
"/api/v1/projects/:name/ga/coverage",
|
|
175
|
+
"/api/v1/projects/:name/ga/measurement-analysis",
|
|
176
|
+
"/api/v1/projects/:name/ga/session-history",
|
|
177
|
+
"/api/v1/projects/:name/ga/social-referral-history",
|
|
178
|
+
"/api/v1/projects/:name/ga/social-referral-trend",
|
|
179
|
+
"/api/v1/projects/:name/ga/status",
|
|
180
|
+
"/api/v1/projects/:name/ga/traffic",
|
|
181
|
+
"/api/v1/projects/:name/gbp/attributes",
|
|
182
|
+
"/api/v1/projects/:name/gbp/keywords",
|
|
183
|
+
"/api/v1/projects/:name/gbp/locations",
|
|
184
|
+
"/api/v1/projects/:name/gbp/lodging",
|
|
185
|
+
"/api/v1/projects/:name/gbp/metrics",
|
|
186
|
+
"/api/v1/projects/:name/gbp/place-actions",
|
|
187
|
+
"/api/v1/projects/:name/gbp/places",
|
|
188
|
+
"/api/v1/projects/:name/gbp/summary",
|
|
189
|
+
"/api/v1/projects/:name/google-ads/performance",
|
|
190
|
+
"/api/v1/projects/:name/google-ads/snapshots",
|
|
191
|
+
"/api/v1/projects/:name/google-ads/snapshots/:snapshotId",
|
|
192
|
+
"/api/v1/projects/:name/google-ads/status",
|
|
193
|
+
"/api/v1/projects/:name/google/connections",
|
|
194
|
+
"/api/v1/projects/:name/google/gsc/coverage",
|
|
195
|
+
"/api/v1/projects/:name/google/gsc/coverage/history",
|
|
196
|
+
"/api/v1/projects/:name/google/gsc/deindexed",
|
|
197
|
+
"/api/v1/projects/:name/google/gsc/inspections",
|
|
198
|
+
"/api/v1/projects/:name/google/gsc/performance",
|
|
199
|
+
"/api/v1/projects/:name/google/gsc/performance/daily",
|
|
200
|
+
"/api/v1/projects/:name/google/gsc/top-pages",
|
|
201
|
+
"/api/v1/projects/:name/gtm/snapshots",
|
|
202
|
+
"/api/v1/projects/:name/gtm/snapshots/:snapshotId",
|
|
203
|
+
"/api/v1/projects/:name/gtm/status",
|
|
204
|
+
"/api/v1/projects/:name/health/history",
|
|
205
|
+
"/api/v1/projects/:name/health/latest",
|
|
206
|
+
"/api/v1/projects/:name/history",
|
|
207
|
+
"/api/v1/projects/:name/insights",
|
|
208
|
+
"/api/v1/projects/:name/insights/:id",
|
|
209
|
+
"/api/v1/projects/:name/keywords",
|
|
210
|
+
"/api/v1/projects/:name/locations",
|
|
211
|
+
"/api/v1/projects/:name/measurement-changes",
|
|
212
|
+
"/api/v1/projects/:name/measurement-data-quality",
|
|
213
|
+
"/api/v1/projects/:name/measurement-overview",
|
|
214
|
+
"/api/v1/projects/:name/measurement-plan",
|
|
215
|
+
"/api/v1/projects/:name/measurement-plan/draft",
|
|
216
|
+
"/api/v1/projects/:name/measurement-plan/draft/assignments",
|
|
217
|
+
"/api/v1/projects/:name/measurement-plan/draft/groups",
|
|
218
|
+
"/api/v1/projects/:name/measurement-plan/draft/targets",
|
|
219
|
+
"/api/v1/projects/:name/measurement-plan/versions",
|
|
220
|
+
"/api/v1/projects/:name/measurement-plan/versions/:revision",
|
|
221
|
+
"/api/v1/projects/:name/measurement-portfolio-summary",
|
|
222
|
+
"/api/v1/projects/:name/measurement-property-competitors",
|
|
223
|
+
"/api/v1/projects/:name/measurement-property-evidence",
|
|
224
|
+
"/api/v1/projects/:name/measurement-property-questions",
|
|
225
|
+
"/api/v1/projects/:name/measurement-query-sets",
|
|
226
|
+
"/api/v1/projects/:name/measurement-query-sets/:setId",
|
|
227
|
+
"/api/v1/projects/:name/measurement-query-templates",
|
|
228
|
+
"/api/v1/projects/:name/measurement-question-result",
|
|
229
|
+
"/api/v1/projects/:name/measurement-report",
|
|
230
|
+
"/api/v1/projects/:name/measurement-setup",
|
|
231
|
+
"/api/v1/projects/:name/notifications",
|
|
232
|
+
"/api/v1/projects/:name/overview",
|
|
233
|
+
"/api/v1/projects/:name/queries",
|
|
234
|
+
"/api/v1/projects/:name/query-tracking",
|
|
235
|
+
"/api/v1/projects/:name/report",
|
|
236
|
+
"/api/v1/projects/:name/report.html",
|
|
237
|
+
"/api/v1/projects/:name/research/runs",
|
|
238
|
+
"/api/v1/projects/:name/research/runs/:runId",
|
|
239
|
+
"/api/v1/projects/:name/results/export",
|
|
240
|
+
"/api/v1/projects/:name/runs",
|
|
241
|
+
"/api/v1/projects/:name/runs/latest",
|
|
242
|
+
"/api/v1/projects/:name/schedule",
|
|
243
|
+
"/api/v1/projects/:name/schedules",
|
|
244
|
+
"/api/v1/projects/:name/search",
|
|
245
|
+
"/api/v1/projects/:name/snapshots",
|
|
246
|
+
"/api/v1/projects/:name/snapshots/diff",
|
|
247
|
+
"/api/v1/projects/:name/technical-aeo",
|
|
248
|
+
"/api/v1/projects/:name/technical-aeo/changes",
|
|
249
|
+
"/api/v1/projects/:name/technical-aeo/crawl",
|
|
250
|
+
"/api/v1/projects/:name/technical-aeo/crawl/pages",
|
|
251
|
+
"/api/v1/projects/:name/technical-aeo/crawl/pages/audit",
|
|
252
|
+
"/api/v1/projects/:name/technical-aeo/dead-links",
|
|
253
|
+
"/api/v1/projects/:name/technical-aeo/graph",
|
|
254
|
+
"/api/v1/projects/:name/technical-aeo/internal-links",
|
|
255
|
+
"/api/v1/projects/:name/technical-aeo/internal-links/neighbors",
|
|
256
|
+
"/api/v1/projects/:name/technical-aeo/pages",
|
|
257
|
+
"/api/v1/projects/:name/technical-aeo/path",
|
|
258
|
+
"/api/v1/projects/:name/technical-aeo/runs",
|
|
259
|
+
"/api/v1/projects/:name/technical-aeo/runs/:runId/page-health-preview",
|
|
260
|
+
"/api/v1/projects/:name/technical-aeo/runs/:runId/progress",
|
|
261
|
+
"/api/v1/projects/:name/technical-aeo/structure",
|
|
262
|
+
"/api/v1/projects/:name/technical-aeo/subgraph",
|
|
263
|
+
"/api/v1/projects/:name/technical-aeo/trend",
|
|
264
|
+
"/api/v1/projects/:name/timeline",
|
|
265
|
+
"/api/v1/projects/:name/traffic/events",
|
|
266
|
+
"/api/v1/projects/:name/traffic/sources",
|
|
267
|
+
"/api/v1/projects/:name/traffic/sources/:id",
|
|
268
|
+
"/api/v1/projects/:name/traffic/status",
|
|
269
|
+
"/api/v1/projects/:name/visibility-compare",
|
|
270
|
+
"/api/v1/projects/:name/visibility-report",
|
|
271
|
+
"/api/v1/projects/:name/visibility-stats",
|
|
272
|
+
"/api/v1/runs",
|
|
273
|
+
"/api/v1/runs/:id",
|
|
274
|
+
"/api/v1/session"
|
|
275
|
+
]);
|
|
276
|
+
function isDemoApiReadAllowed(method, registeredRoute) {
|
|
277
|
+
return (method === "GET" || method === "HEAD") && registeredRoute !== void 0 && DEMO_READ_ROUTES.has(registeredRoute);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/demo/http.ts
|
|
281
|
+
var DEMO_VIEWER = { id: "public-demo-viewer", name: "Public demo", scopes: ["read"], projectId: null };
|
|
282
|
+
var DEMO_CLIENT_CONFIG = {
|
|
283
|
+
demo: { enabled: true, readOnly: true, sampleData: true },
|
|
284
|
+
dashboard: { showAgentBar: false, showUpdateNotification: false }
|
|
285
|
+
};
|
|
286
|
+
var INLINE_SCRIPT = /<script(?<attributes>[^>]*)>(?<body>[\s\S]*?)<\/script>/gi;
|
|
287
|
+
var DEMO_DOCUMENT_PATH = /^\/(?:projects(?:\/[^/.]+(?:\/(?:portfolio|discovery|search-console|activity|technical-aeo|conversions|local|queries|backlinks|report|history|settings|properties)(?:\/[^/.]+)*)?)?|runs|history|traffic(?:\/[^/.]+(?:\/[^/.]+)?)?)\/?$/;
|
|
288
|
+
var UNDECODABLE_PATH = "/.undecodable-path";
|
|
289
|
+
var DEMO_REPORT_DISCLOSURE = '<aside role="note" style="box-sizing:border-box;margin:0;padding:12px 24px;background:#fff7d6;border-bottom:1px solid #e5c75c;color:#4a3a00;font:600 14px/1.5 system-ui,sans-serif;text-align:center">Public Canonry demo: this report contains fictional sample data from stored demo records. No live provider query produced it.</aside>';
|
|
290
|
+
function demoContentSecurityPolicy(document) {
|
|
291
|
+
const inlineHashes = [...document.matchAll(INLINE_SCRIPT)].filter((match) => !/\ssrc\s*=/i.test(match.groups?.attributes ?? "")).map((match) => `'sha256-${createHash("sha256").update(match.groups?.body ?? "", "utf8").digest("base64")}'`);
|
|
292
|
+
return [
|
|
293
|
+
"default-src 'self'",
|
|
294
|
+
["script-src 'self'", ...inlineHashes].join(" "),
|
|
295
|
+
"style-src 'self' 'unsafe-inline'",
|
|
296
|
+
"img-src 'self' data:",
|
|
297
|
+
"font-src 'self'",
|
|
298
|
+
"connect-src 'self'",
|
|
299
|
+
"worker-src 'self' blob:",
|
|
300
|
+
"object-src 'none'",
|
|
301
|
+
"base-uri 'self'",
|
|
302
|
+
"form-action 'none'",
|
|
303
|
+
"frame-ancestors 'self'"
|
|
304
|
+
].join("; ");
|
|
305
|
+
}
|
|
306
|
+
function isDemoDocumentRequest(request) {
|
|
307
|
+
return (request.method === "GET" || request.method === "HEAD") && DEMO_DOCUMENT_PATH.test(request.url.split("?")[0] ?? "");
|
|
308
|
+
}
|
|
309
|
+
function isOutsideDemoApiBudget(request) {
|
|
310
|
+
const route = request.routeOptions.url;
|
|
311
|
+
return route === void 0 ? isDemoDocumentRequest(request) : !route.startsWith("/api/");
|
|
312
|
+
}
|
|
313
|
+
async function createDemoHttpServer(options) {
|
|
314
|
+
const { db, assetsDir, now } = options;
|
|
315
|
+
const html = readFileSync(join(assetsDir, "index.html"), "utf8").replace("<head>", '<head><base href="/">').replace("</head>", `<script>window.__CANONRY_CONFIG__=${JSON.stringify(DEMO_CLIENT_CONFIG)}</script></head>`);
|
|
316
|
+
const contentSecurityPolicy = demoContentSecurityPolicy(html);
|
|
317
|
+
db.insert(apiKeys).values({
|
|
318
|
+
...DEMO_VIEWER,
|
|
319
|
+
keyHash: randomUUID(),
|
|
320
|
+
keyPrefix: "demo",
|
|
321
|
+
createdAt: now.toISOString()
|
|
322
|
+
}).run();
|
|
323
|
+
const undecodable = /* @__PURE__ */ new WeakSet();
|
|
324
|
+
const app = Fastify({
|
|
325
|
+
logger: false,
|
|
326
|
+
bodyLimit: 1024 * 1024,
|
|
327
|
+
// The API budget is keyed by visitor address. Believe X-Forwarded-For only
|
|
328
|
+
// from the named proxies (a same-machine proxy by default), so a directly
|
|
329
|
+
// connected client cannot pick the address it is counted under.
|
|
330
|
+
trustProxy: [...options.trustProxy ?? DEFAULT_DEMO_TRUSTED_PROXIES],
|
|
331
|
+
// The router answers an undecodable path itself, before any hook, so the
|
|
332
|
+
// request would skip the visitor's budget. Route it again under a fixed
|
|
333
|
+
// path instead: the normal hooks then charge it to the forwarded visitor
|
|
334
|
+
// address and add the usual headers before the not-found handler refuses it.
|
|
335
|
+
frameworkErrors: (error, request, reply) => {
|
|
336
|
+
if (error.code !== "FST_ERR_BAD_URL") {
|
|
337
|
+
const body = JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred" } });
|
|
338
|
+
reply.raw.writeHead(500, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
|
|
339
|
+
reply.raw.end(body);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
undecodable.add(request.raw);
|
|
343
|
+
request.raw.url = UNDECODABLE_PATH;
|
|
344
|
+
app.routing(request.raw, reply.raw);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
await app.register(rateLimit, {
|
|
348
|
+
// Not attached per route: route hooks never run for the not-found handler
|
|
349
|
+
// and run after the refusals below. The single hook charges first instead.
|
|
350
|
+
global: false,
|
|
351
|
+
max: options.apiRateLimitMax ?? 600,
|
|
352
|
+
timeWindow: "1 minute",
|
|
353
|
+
// A dashboard load fans out across many immutable chunks. Those reads do
|
|
354
|
+
// not touch the API budget, which stays available for stored-data calls.
|
|
355
|
+
allowList: isOutsideDemoApiBudget
|
|
356
|
+
});
|
|
357
|
+
const chargeApiBudget = app.rateLimit();
|
|
358
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
359
|
+
reply.header("X-Robots-Tag", "noindex, nofollow");
|
|
360
|
+
reply.header("X-Content-Type-Options", "nosniff");
|
|
361
|
+
reply.header("Referrer-Policy", "no-referrer");
|
|
362
|
+
reply.header("Content-Security-Policy", contentSecurityPolicy);
|
|
363
|
+
await chargeApiBudget.call(app, request, reply);
|
|
364
|
+
const method = request.method;
|
|
365
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
366
|
+
return reply.code(403).send({ error: { code: "DEMO_READ_ONLY", message: "This public demo is view only. Changes and live runs are disabled." } });
|
|
367
|
+
}
|
|
368
|
+
const route = request.routeOptions.url;
|
|
369
|
+
if (request.url.startsWith("/api/") || route?.startsWith("/api/")) {
|
|
370
|
+
if (!isDemoApiReadAllowed(method, route)) {
|
|
371
|
+
return reply.code(403).send({ error: { code: "DEMO_READ_ONLY", message: "This action is unavailable in the view-only demo." } });
|
|
372
|
+
}
|
|
373
|
+
request.apiKey = { ...DEMO_VIEWER };
|
|
374
|
+
request.principal = { kind: "api-key", ...DEMO_VIEWER, viaCookie: false };
|
|
375
|
+
reply.header("Cache-Control", "no-store");
|
|
376
|
+
if (route === "/api/v1/projects/:name/bing/coverage") {
|
|
377
|
+
const name = request.params.name;
|
|
378
|
+
const project = db.select().from(projects).where(eq(projects.name, name)).get();
|
|
379
|
+
if (!project) return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Project not found." } });
|
|
380
|
+
const rows = db.select().from(bingUrlInspections).where(eq(bingUrlInspections.projectId, project.id)).orderBy(desc(bingUrlInspections.inspectedAt)).all();
|
|
381
|
+
const latest = /* @__PURE__ */ new Map();
|
|
382
|
+
for (const row of rows) if (!latest.has(row.url)) latest.set(row.url, row);
|
|
383
|
+
const format = (row) => ({ url: row.url, httpCode: row.httpCode, inIndex: row.inIndex, lastCrawledDate: row.lastCrawledDate, inIndexDate: row.inIndexDate, inspectedAt: row.inspectedAt, documentSize: row.documentSize, anchorCount: row.anchorCount, discoveryDate: row.discoveryDate });
|
|
384
|
+
const indexed = [...latest.values()].filter((row) => row.inIndex === true).map(format);
|
|
385
|
+
const notIndexed = [...latest.values()].filter((row) => row.inIndex === false).map(format);
|
|
386
|
+
const unknown = [...latest.values()].filter((row) => row.inIndex === null).map(format);
|
|
387
|
+
const total = latest.size;
|
|
388
|
+
return reply.send({ summary: { total, indexed: indexed.length, notIndexed: notIndexed.length, unknown: unknown.length, percentage: total ? Math.round(indexed.length / total * 1e3) / 10 : 0 }, lastInspectedAt: rows[0]?.inspectedAt ?? null, indexed, notIndexed, unknown });
|
|
389
|
+
}
|
|
390
|
+
if (route === "/api/v1/projects/:name/bing/performance") {
|
|
391
|
+
const name = request.params.name;
|
|
392
|
+
const project = db.select().from(projects).where(eq(projects.name, name)).get();
|
|
393
|
+
if (!project) return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Project not found." } });
|
|
394
|
+
return reply.send(db.select().from(bingKeywordStats).where(eq(bingKeywordStats.projectId, project.id)).limit(200).all().map((row) => ({
|
|
395
|
+
query: row.query,
|
|
396
|
+
impressions: row.impressions,
|
|
397
|
+
clicks: row.clicks,
|
|
398
|
+
ctr: Number(row.ctr),
|
|
399
|
+
averagePosition: Number(row.averagePosition)
|
|
400
|
+
})));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
app.addHook("onSend", async (request, reply, payload) => {
|
|
405
|
+
if (request.routeOptions.url !== "/api/v1/projects/:name/report.html" || typeof payload !== "string" || !String(reply.getHeader("content-type")).startsWith("text/html")) {
|
|
406
|
+
return payload;
|
|
407
|
+
}
|
|
408
|
+
reply.removeHeader("content-length");
|
|
409
|
+
return payload.replace(/<body([^>]*)>/i, `<body$1>${DEMO_REPORT_DISCLOSURE}`);
|
|
410
|
+
});
|
|
411
|
+
app.get("/health", async () => ({ status: "ok", service: "canonry-demo", version: PACKAGE_VERSION, demo: true, workerEnabled: false }));
|
|
412
|
+
app.get("/api/v1/session", async () => ({ authenticated: true, setupRequired: false }));
|
|
413
|
+
app.get("/api/v1/demo", async () => ({ mode: "view-only", sampleData: true, seededAt: now.toISOString() }));
|
|
414
|
+
await app.register(apiRoutes, { db, skipAuth: true, ...options.readOptions });
|
|
415
|
+
await app.register(fastifyStatic, {
|
|
416
|
+
root: join(assetsDir, "assets"),
|
|
417
|
+
prefix: "/assets/",
|
|
418
|
+
index: false,
|
|
419
|
+
dotfiles: "deny"
|
|
420
|
+
});
|
|
421
|
+
const sendDocument = (_request, reply) => {
|
|
422
|
+
return reply.header("Cache-Control", "no-cache").type("text/html").send(html);
|
|
423
|
+
};
|
|
424
|
+
for (const [filename, contentType] of [["favicon.svg", "image/svg+xml"], ["favicon-32.png", "image/png"], ["apple-touch-icon.png", "image/png"]]) {
|
|
425
|
+
const path = join(assetsDir, filename);
|
|
426
|
+
if (existsSync(path)) {
|
|
427
|
+
const icon = readFileSync(path);
|
|
428
|
+
app.get(`/${filename}`, async (_request, reply) => reply.type(contentType).send(icon));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
app.get("/", sendDocument);
|
|
432
|
+
app.get("/robots.txt", async (_request, reply) => reply.type("text/plain").send("User-agent: *\nDisallow: /\n"));
|
|
433
|
+
app.setNotFoundHandler(async (request, reply) => {
|
|
434
|
+
if (undecodable.has(request.raw)) {
|
|
435
|
+
return reply.code(400).send({ error: { code: "VALIDATION_ERROR", message: "The request path is not a valid URL." } });
|
|
436
|
+
}
|
|
437
|
+
if (isDemoDocumentRequest(request)) return sendDocument(request, reply);
|
|
438
|
+
return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Page not found." } });
|
|
439
|
+
});
|
|
440
|
+
return app;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/demo/seed-marketing.ts
|
|
444
|
+
var checksum = "d".repeat(64);
|
|
445
|
+
function dateAt(now, offset) {
|
|
446
|
+
const result = new Date(now);
|
|
447
|
+
result.setUTCDate(result.getUTCDate() + offset);
|
|
448
|
+
return result.toISOString().slice(0, 10);
|
|
449
|
+
}
|
|
450
|
+
function seedDemoMarketing(db, context) {
|
|
451
|
+
for (const project of [context.simple, context.portfolio]) seedProjectMarketing(db, project, context.now);
|
|
452
|
+
}
|
|
453
|
+
function seedProjectMarketing(db, project, now) {
|
|
454
|
+
const capturedAt = now.toISOString();
|
|
455
|
+
const prefix = `demo-marketing-${project.id}`;
|
|
456
|
+
const campaignId = `demo-signals-${project.id}-campaign`;
|
|
457
|
+
const adsConnectionId = `${prefix}-google-ads`;
|
|
458
|
+
const gtmConnectionId = `${prefix}-gtm`;
|
|
459
|
+
const adsRunId = `${prefix}-google-ads-run`;
|
|
460
|
+
const gtmRunId = `${prefix}-gtm-run`;
|
|
461
|
+
const customerSnapshotId = `${prefix}-customers`;
|
|
462
|
+
const inventorySnapshotId = `${prefix}-inventory`;
|
|
463
|
+
const metricsSnapshotId = `${prefix}-metrics`;
|
|
464
|
+
const gtmSnapshotId = `${prefix}-gtm-live`;
|
|
465
|
+
db.insert(googleConnections).values({
|
|
466
|
+
id: `${prefix}-gbp`,
|
|
467
|
+
domain: project.domain,
|
|
468
|
+
connectionType: "gbp",
|
|
469
|
+
propertyId: `accounts/sample-${project.id}`,
|
|
470
|
+
scopes: [],
|
|
471
|
+
createdByProjectId: project.id,
|
|
472
|
+
createdAt: capturedAt,
|
|
473
|
+
updatedAt: capturedAt
|
|
474
|
+
}).run();
|
|
475
|
+
db.insert(runs).values([
|
|
476
|
+
{ id: adsRunId, projectId: project.id, kind: "google-ads-sync", status: "completed", trigger: "manual", createdAt: capturedAt, finishedAt: capturedAt },
|
|
477
|
+
{ id: gtmRunId, projectId: project.id, kind: "gtm-sync", status: "completed", trigger: "manual", createdAt: capturedAt, finishedAt: capturedAt }
|
|
478
|
+
]).run();
|
|
479
|
+
db.insert(googleAdsConnections).values({
|
|
480
|
+
id: adsConnectionId,
|
|
481
|
+
projectId: project.id,
|
|
482
|
+
selectedCustomerId: "demo-customer",
|
|
483
|
+
selectedCustomerName: "Sample Google Ads account",
|
|
484
|
+
selectedCustomerCurrencyCode: "USD",
|
|
485
|
+
selectedCustomerTimeZone: "UTC",
|
|
486
|
+
selectedCustomerStatus: "enabled",
|
|
487
|
+
scopes: [],
|
|
488
|
+
lastValidatedAt: capturedAt,
|
|
489
|
+
lastCustomerSnapshotId: customerSnapshotId,
|
|
490
|
+
lastInventorySnapshotAt: capturedAt,
|
|
491
|
+
lastInventorySnapshotId: inventorySnapshotId,
|
|
492
|
+
lastMetricsSnapshotAt: capturedAt,
|
|
493
|
+
lastMetricsSnapshotId: metricsSnapshotId,
|
|
494
|
+
createdAt: capturedAt,
|
|
495
|
+
updatedAt: capturedAt
|
|
496
|
+
}).run();
|
|
497
|
+
db.insert(gtmConnections).values({
|
|
498
|
+
id: gtmConnectionId,
|
|
499
|
+
projectId: project.id,
|
|
500
|
+
selectedAccountId: "demo-account",
|
|
501
|
+
selectedAccountName: "Sample account",
|
|
502
|
+
selectedContainerId: "demo-container",
|
|
503
|
+
selectedContainerName: "Sample web container",
|
|
504
|
+
selectedContainerPublicId: "GTM-SAMPLE",
|
|
505
|
+
selectedWorkspaceId: "demo-workspace",
|
|
506
|
+
selectedWorkspaceName: "Sample workspace",
|
|
507
|
+
scopes: [],
|
|
508
|
+
lastValidatedAt: capturedAt,
|
|
509
|
+
lastSnapshotAt: capturedAt,
|
|
510
|
+
lastSnapshotId: gtmSnapshotId,
|
|
511
|
+
createdAt: capturedAt,
|
|
512
|
+
updatedAt: capturedAt
|
|
513
|
+
}).run();
|
|
514
|
+
db.insert(googleAdsRawSnapshots).values([
|
|
515
|
+
{
|
|
516
|
+
id: customerSnapshotId,
|
|
517
|
+
projectId: project.id,
|
|
518
|
+
connectionId: adsConnectionId,
|
|
519
|
+
runId: adsRunId,
|
|
520
|
+
kind: "accessible-customers",
|
|
521
|
+
customerId: "demo-customer",
|
|
522
|
+
payloadChecksum: checksum,
|
|
523
|
+
rawPayloadSha256: null,
|
|
524
|
+
rawPayloadBytes: null,
|
|
525
|
+
redactedFieldCount: 0,
|
|
526
|
+
capturedAt,
|
|
527
|
+
createdAt: capturedAt,
|
|
528
|
+
payload: {
|
|
529
|
+
kind: "accessible-customers",
|
|
530
|
+
data: {
|
|
531
|
+
customers: [{
|
|
532
|
+
resourceName: "customers/demo-customer",
|
|
533
|
+
customerId: "demo-customer",
|
|
534
|
+
parentCustomerId: null,
|
|
535
|
+
descriptiveName: "Sample Google Ads account",
|
|
536
|
+
currencyCode: "USD",
|
|
537
|
+
timeZone: "UTC",
|
|
538
|
+
manager: false,
|
|
539
|
+
hidden: false,
|
|
540
|
+
testAccount: true,
|
|
541
|
+
level: 0,
|
|
542
|
+
status: "enabled"
|
|
543
|
+
}],
|
|
544
|
+
totalAccessible: 1,
|
|
545
|
+
truncated: false,
|
|
546
|
+
selection: { loginCustomerId: null, customerId: "demo-customer", selectedAt: capturedAt },
|
|
547
|
+
fetchedAt: capturedAt
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: inventorySnapshotId,
|
|
553
|
+
projectId: project.id,
|
|
554
|
+
connectionId: adsConnectionId,
|
|
555
|
+
runId: adsRunId,
|
|
556
|
+
kind: "inventory",
|
|
557
|
+
customerId: "demo-customer",
|
|
558
|
+
payloadChecksum: checksum,
|
|
559
|
+
rawPayloadSha256: null,
|
|
560
|
+
rawPayloadBytes: null,
|
|
561
|
+
redactedFieldCount: 0,
|
|
562
|
+
capturedAt,
|
|
563
|
+
createdAt: capturedAt,
|
|
564
|
+
payload: {
|
|
565
|
+
kind: "inventory",
|
|
566
|
+
data: {
|
|
567
|
+
customerId: "demo-customer",
|
|
568
|
+
fetchedAt: capturedAt,
|
|
569
|
+
campaigns: [{ id: campaignId, resourceName: `customers/demo-customer/campaigns/${campaignId}`, name: "Sample search campaign", status: "enabled", advertisingChannelType: "SEARCH", biddingStrategyType: "MAXIMIZE_CONVERSIONS" }],
|
|
570
|
+
conversionActions: [{ id: "demo-action", resourceName: "customers/demo-customer/conversionActions/demo-action", name: "Sample lead", status: "enabled", category: "SUBMIT_LEAD_FORM", origin: "WEBSITE", primaryForGoal: true, includeInConversionsMetric: true }],
|
|
571
|
+
customerConversionGoals: [{ category: "SUBMIT_LEAD_FORM", origin: "WEBSITE", biddable: true }],
|
|
572
|
+
campaignConversionGoals: [],
|
|
573
|
+
customConversionGoals: [],
|
|
574
|
+
campaignGoalConfigurations: [{ campaignId, goalConfigLevel: "customer", customGoalId: null }]
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
id: metricsSnapshotId,
|
|
580
|
+
projectId: project.id,
|
|
581
|
+
connectionId: adsConnectionId,
|
|
582
|
+
runId: adsRunId,
|
|
583
|
+
kind: "campaign-metrics",
|
|
584
|
+
customerId: "demo-customer",
|
|
585
|
+
payloadChecksum: checksum,
|
|
586
|
+
rawPayloadSha256: null,
|
|
587
|
+
rawPayloadBytes: null,
|
|
588
|
+
redactedFieldCount: 0,
|
|
589
|
+
capturedAt,
|
|
590
|
+
createdAt: capturedAt,
|
|
591
|
+
payload: {
|
|
592
|
+
kind: "campaign-metrics",
|
|
593
|
+
data: {
|
|
594
|
+
query: { campaignIds: [campaignId], startDate: dateAt(now, -30), endDate: dateAt(now, 0) },
|
|
595
|
+
rows: Array.from({ length: 14 }, (_, index) => ({ campaignId, date: dateAt(now, -14 + index), impressions: 260 + index * 17, clicks: 14 + index, costMicros: 15e5 + index * 75e3, conversions: 1 + index % 3 / 2, conversionValueMicros: 75e5 + index * 3e5 })),
|
|
596
|
+
truncated: false,
|
|
597
|
+
fetchedAt: capturedAt
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
]).run();
|
|
602
|
+
db.insert(gtmRawSnapshots).values({
|
|
603
|
+
id: gtmSnapshotId,
|
|
604
|
+
projectId: project.id,
|
|
605
|
+
connectionId: gtmConnectionId,
|
|
606
|
+
runId: gtmRunId,
|
|
607
|
+
kind: "container",
|
|
608
|
+
accountId: "demo-account",
|
|
609
|
+
containerId: "demo-container",
|
|
610
|
+
workspaceId: "demo-workspace",
|
|
611
|
+
payloadChecksum: checksum,
|
|
612
|
+
rawPayloadSha256: null,
|
|
613
|
+
rawPayloadBytes: null,
|
|
614
|
+
redactedFieldCount: 0,
|
|
615
|
+
capturedAt,
|
|
616
|
+
createdAt: capturedAt,
|
|
617
|
+
payload: {
|
|
618
|
+
kind: "container",
|
|
619
|
+
data: {
|
|
620
|
+
account: { id: "demo-account", path: "accounts/demo-account", name: "Sample account", shareData: null },
|
|
621
|
+
container: { accountId: "demo-account", id: "demo-container", path: "accounts/demo-account/containers/demo-container", name: "Sample web container", publicId: "GTM-SAMPLE", domainName: project.domain, usageContexts: ["web"] },
|
|
622
|
+
workspaces: [],
|
|
623
|
+
draft: null,
|
|
624
|
+
fetchedAt: capturedAt,
|
|
625
|
+
live: {
|
|
626
|
+
source: "live",
|
|
627
|
+
version: { accountId: "demo-account", containerId: "demo-container", id: "demo-version", path: "accounts/demo-account/containers/demo-container/versions/demo-version", name: "Sample published version", description: null, fingerprint: "sample-version", deleted: false },
|
|
628
|
+
graph: {
|
|
629
|
+
accountId: "demo-account",
|
|
630
|
+
containerId: "demo-container",
|
|
631
|
+
workspaceId: null,
|
|
632
|
+
tags: [{ id: "demo-tag", name: "Sample lead conversion", type: "awct", paused: false, firingTriggerIds: ["demo-trigger"], blockingTriggerIds: [], referencedVariableIds: ["demo-value", "demo-transaction", "demo-currency"], parameterKeys: ["conversionId", "conversionLabel", "conversionValue", "orderId", "currencyCode"], fingerprint: "sample-tag" }],
|
|
633
|
+
triggers: [{ id: "demo-trigger", name: "generate_lead", type: "customEvent", customEventNames: ["generate_lead"], filterKeys: ["event"], autoEventFilterKeys: [], fingerprint: "sample-trigger" }],
|
|
634
|
+
variables: [
|
|
635
|
+
{ id: "demo-value", name: "value", type: "v", dataLayerVariableName: "value", parameterKeys: [], fingerprint: "sample-value" },
|
|
636
|
+
{ id: "demo-transaction", name: "transaction_id", type: "v", dataLayerVariableName: "transaction_id", parameterKeys: [], fingerprint: "sample-transaction" },
|
|
637
|
+
{ id: "demo-currency", name: "currency", type: "v", dataLayerVariableName: "currency", parameterKeys: [], fingerprint: "sample-currency" }
|
|
638
|
+
],
|
|
639
|
+
googleAdsTagAssessments: [{ tagId: "demo-tag", tagType: "awct", recognition: "recognized", recognitionReason: null, conversionId: { source: "literal", literal: "AW-123456", variableRef: null }, conversionLabel: { source: "literal", literal: "demo-label", variableRef: null }, value: { source: "variable-ref", literal: null, variableRef: "{{value}}" }, transactionId: { source: "variable-ref", literal: null, variableRef: "{{transaction_id}}" }, currency: { source: "variable-ref", literal: null, variableRef: "{{currency}}" }, triggerStrategy: "custom-event", triggerIds: ["demo-trigger"], triggerPredicates: [{ triggerId: "demo-trigger", triggerType: "customEvent", eventPredicates: [{ operator: "equals", value: "generate_lead", negated: false, ignoreCase: false }], hostnamePredicates: [{ operator: "equals", value: project.domain, negated: false, ignoreCase: false }], unsupportedConditionCount: 0 }], reviewReasons: [] }]
|
|
640
|
+
},
|
|
641
|
+
fetchedAt: capturedAt
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}).run();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/demo/seed-exploration.ts
|
|
649
|
+
import { and, eq as eq2 } from "drizzle-orm";
|
|
650
|
+
function seedDemoExploration(db, context) {
|
|
651
|
+
const createdAt = context.now.toISOString();
|
|
652
|
+
for (const project of [context.simple, context.portfolio]) {
|
|
653
|
+
const sourceId = `${project.id}-traffic`;
|
|
654
|
+
const shared = { projectId: project.id, sourceId, createdAt, updatedAt: createdAt };
|
|
655
|
+
db.insert(trafficSources).values({ id: sourceId, projectId: project.id, sourceType: "cloudflare", displayName: `${project.displayName} website`, status: "active", lastSyncedAt: createdAt, configJson: { zoneName: project.domain }, createdAt, updatedAt: createdAt }).run();
|
|
656
|
+
for (let hour = 1; hour <= 24; hour++) {
|
|
657
|
+
const date = new Date(context.now.getTime() - hour * 36e5);
|
|
658
|
+
date.setUTCMinutes(0, 0, 0);
|
|
659
|
+
const tsHour = date.toISOString();
|
|
660
|
+
db.insert(crawlerEventsHourly).values({ ...shared, tsHour, botId: "gptbot", operator: "openai", verificationStatus: "claimed_unverified", pathNormalized: "/", status: 200, hits: 3 + hour % 4 }).run();
|
|
661
|
+
db.insert(aiUserFetchEventsHourly).values({ ...shared, tsHour, botId: "chatgpt-user", operator: "openai", verificationStatus: "claimed_unverified", pathNormalized: "/services/", status: 200, hits: 1 + hour % 2 }).run();
|
|
662
|
+
db.insert(aiReferralEventsHourly).values({ ...shared, tsHour, product: "chatgpt", operator: "openai", sourceDomain: "demo-answer-engine.example", evidenceType: "referer", landingPathNormalized: "/services/", status: 200, sessionsOrHits: 2, organicSessionsOrHits: 2 }).run();
|
|
663
|
+
}
|
|
664
|
+
const runId = `demo-signals-${project.id}-crawl`;
|
|
665
|
+
const root = `https://${project.domain}/`;
|
|
666
|
+
seedPageAudits(db, project.id, runId, root, createdAt);
|
|
667
|
+
const researchRunId = `${project.id}-research`;
|
|
668
|
+
db.insert(researchRuns).values({ id: researchRunId, projectId: project.id, status: "completed", provider: "openai", requestedModel: "demo-model", resolvedModel: "demo-model", totalQueries: 2, completedQueries: 2, failedQueries: 0, startedAt: createdAt, finishedAt: createdAt, createdAt }).run();
|
|
669
|
+
const topics = project.id === context.simple.id ? ["How do homeowners compare roofing materials?", "What should a roof repair estimate include?"] : ["What amenities matter most for a family resort stay?", "How should travelers compare coastal resorts?"];
|
|
670
|
+
const discoveryId = `${project.id}-discovery`;
|
|
671
|
+
db.insert(discoverySessions).values({ id: discoveryId, projectId: project.id, status: "completed", icpDescription: `Fictional research example for ${project.displayName}.`, buyerDescription: "People comparing local services and planning a purchase.", seedProvider: "openai", seedProviders: ["openai"], seedCountRaw: 2, seedCount: 2, canonicalCount: 2, probeCount: 2, citedCount: 1, aspirationalCount: 1, wastedCount: 0, startedAt: createdAt, finishedAt: createdAt, createdAt }).run();
|
|
672
|
+
db.insert(discoveryProbes).values(topics.map((query, i) => ({ id: `${discoveryId}-${i}`, sessionId: discoveryId, projectId: project.id, query, bucket: i === 0 ? "cited" : "aspirational", citationState: i === 0 ? "cited" : "not-cited", answerMentioned: i === 0, citedDomains: i === 0 ? [project.domain] : ["comparison-guide.example"], createdAt }))).run();
|
|
673
|
+
db.insert(researchRunQueries).values(topics.map((queryText, position) => ({ id: `${researchRunId}-${position}`, researchRunId, position, queryText, status: "completed", requestedModel: "demo-model", resolvedModel: "demo-model", servedModel: "demo-model", answerText: `Fictional sample answer: compare clear pricing, service details, and independent reviews. ${project.displayName} illustrates a business with useful comparison guidance.`, groundingSources: [{ uri: `${root}guides/`, title: `${project.displayName} comparison guide` }], citedDomains: [project.domain], answerMentioned: true, citationState: "cited", startedAt: createdAt, finishedAt: createdAt, createdAt }))).run();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function seedPageAudits(db, projectId, runId, root, createdAt) {
|
|
677
|
+
const crawlPages = db.select().from(siteCrawlPages).where(and(eq2(siteCrawlPages.projectId, projectId), eq2(siteCrawlPages.runId, runId))).all();
|
|
678
|
+
const audited = crawlPages.filter((page) => page.auditState === "success" && page.auditScore !== null);
|
|
679
|
+
const pages = audited.map((page, index) => {
|
|
680
|
+
const factors2 = siteAuditPageFactorSchema.array().parse(page.auditFields.factors);
|
|
681
|
+
return { id: `${projectId}-audit-page-${index}`, projectId, runId, url: page.url, overallScore: page.auditScore, status: "success", factors: factors2, createdAt };
|
|
682
|
+
});
|
|
683
|
+
const factors = pages.flatMap((page) => page.factors);
|
|
684
|
+
const factorAverages = [...new Set(factors.map((factor) => factor.id))].map((id) => {
|
|
685
|
+
const matching = factors.filter((factor) => factor.id === id);
|
|
686
|
+
const first = matching[0];
|
|
687
|
+
const avgScore = Math.round(matching.reduce((sum, factor) => sum + factor.score, 0) / matching.length);
|
|
688
|
+
return {
|
|
689
|
+
id,
|
|
690
|
+
name: first.name,
|
|
691
|
+
weight: first.weight,
|
|
692
|
+
avgScore,
|
|
693
|
+
status: factorStatusFromScore(avgScore),
|
|
694
|
+
pagesPassing: matching.filter((factor) => factorStatusFromScore(factor.score) === "pass").length,
|
|
695
|
+
pagesPartial: matching.filter((factor) => factorStatusFromScore(factor.score) === "partial").length,
|
|
696
|
+
pagesFailing: matching.filter((factor) => factorStatusFromScore(factor.score) === "fail").length
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
const crossCuttingIssues = factorAverages.filter((factor) => factor.pagesPartial + factor.pagesFailing > 0).map((factor) => ({
|
|
700
|
+
factorId: factor.id,
|
|
701
|
+
factorName: factor.name,
|
|
702
|
+
avgScore: factor.avgScore,
|
|
703
|
+
affectedPages: factor.pagesPartial + factor.pagesFailing,
|
|
704
|
+
totalPages: pages.length,
|
|
705
|
+
affectedPct: Math.round((factor.pagesPartial + factor.pagesFailing) / pages.length * 100),
|
|
706
|
+
topRecommendations: [`Complete ${factor.name.toLowerCase()} details on the affected pages.`]
|
|
707
|
+
}));
|
|
708
|
+
db.insert(siteAuditSnapshots).values({
|
|
709
|
+
id: `${projectId}-audit`,
|
|
710
|
+
projectId,
|
|
711
|
+
runId,
|
|
712
|
+
sitemapUrl: `${root}sitemap.xml`,
|
|
713
|
+
auditedAt: createdAt,
|
|
714
|
+
aggregateScore: Math.round(pages.reduce((sum, page) => sum + page.overallScore, 0) / pages.length),
|
|
715
|
+
pagesDiscovered: crawlPages.length,
|
|
716
|
+
pagesAudited: pages.length,
|
|
717
|
+
pagesErrored: crawlPages.filter((page) => page.fetchState === "fetch-error").length,
|
|
718
|
+
pagesSkipped: crawlPages.filter((page) => page.auditState === "not-applicable").length,
|
|
719
|
+
factorAverages,
|
|
720
|
+
crossCuttingIssues,
|
|
721
|
+
prioritizedFixes: crossCuttingIssues.flatMap((issue) => issue.topRecommendations),
|
|
722
|
+
createdAt
|
|
723
|
+
}).run();
|
|
724
|
+
for (let i = 0; i < pages.length; i += 100) db.insert(siteAuditPages).values(pages.slice(i, i + 100)).run();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/demo/seed-core.ts
|
|
728
|
+
import crypto from "crypto";
|
|
729
|
+
|
|
730
|
+
// src/demo/portfolio.ts
|
|
731
|
+
var HARBOR_MARKETS = [
|
|
732
|
+
{ key: "key-west", label: "Key West", city: "Key West", region: "FL" },
|
|
733
|
+
{ key: "coastal-maine", label: "Coastal Maine", city: "Portland", region: "ME" },
|
|
734
|
+
{ key: "pacific-northwest", label: "Pacific Northwest", city: "Seattle", region: "WA" }
|
|
735
|
+
];
|
|
736
|
+
function harborProperties() {
|
|
737
|
+
return HARBOR_MARKETS.flatMap((market) => [1, 2, 3, 4].map((number) => ({
|
|
738
|
+
key: `harbor-${market.key}-${number}`,
|
|
739
|
+
number,
|
|
740
|
+
label: `Harbor ${market.label} ${number === 1 ? "Resort" : `Villas ${number}`}`,
|
|
741
|
+
market,
|
|
742
|
+
path: `/destinations/${market.key}/${number === 1 ? "resort" : `villas-${number}`}`
|
|
743
|
+
})));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/demo/seed-answer-intelligence.ts
|
|
747
|
+
import { and as and2, asc, eq as eq3 } from "drizzle-orm";
|
|
748
|
+
function seedDemoAnswerIntelligence(db, project) {
|
|
749
|
+
const service2 = new IntelligenceService(db);
|
|
750
|
+
const sweeps = db.select().from(runs).where(and2(eq3(runs.projectId, project.id), eq3(runs.kind, RunKinds["answer-visibility"]))).orderBy(asc(runs.createdAt)).all();
|
|
751
|
+
for (const [index, sweep] of sweeps.entries()) {
|
|
752
|
+
const previous = index > 0 ? sweeps[index - 1] : null;
|
|
753
|
+
const history = sweeps.slice(Math.max(0, index - (HISTORY_WINDOW_RUNS - 1)), index + 1);
|
|
754
|
+
const result = service2.analyzeRunWithPrevious(sweep, previous, history, { dryRun: true });
|
|
755
|
+
if (!result) continue;
|
|
756
|
+
db.transaction((tx) => {
|
|
757
|
+
tx.insert(healthSnapshots).values({
|
|
758
|
+
id: `${sweep.id}-health`,
|
|
759
|
+
projectId: project.id,
|
|
760
|
+
runId: sweep.id,
|
|
761
|
+
overallCitedRate: String(result.health.overallCitedRate),
|
|
762
|
+
overallMentionRate: String(result.health.overallMentionRate),
|
|
763
|
+
totalPairs: result.health.totalPairs,
|
|
764
|
+
citedPairs: result.health.citedPairs,
|
|
765
|
+
mentionedPairs: result.health.mentionedPairs,
|
|
766
|
+
providerBreakdown: result.health.providerBreakdown,
|
|
767
|
+
createdAt: sweep.createdAt
|
|
768
|
+
}).run();
|
|
769
|
+
const found = previous ? result.insights : [];
|
|
770
|
+
for (const [position, insight] of found.entries()) {
|
|
771
|
+
tx.insert(insights).values({
|
|
772
|
+
id: `${sweep.id}-insight-${position + 1}`,
|
|
773
|
+
projectId: project.id,
|
|
774
|
+
runId: sweep.id,
|
|
775
|
+
type: insight.type,
|
|
776
|
+
severity: insight.severity,
|
|
777
|
+
title: insight.title,
|
|
778
|
+
query: insight.query,
|
|
779
|
+
provider: insight.provider,
|
|
780
|
+
recommendation: insight.recommendation ?? null,
|
|
781
|
+
cause: insight.cause ?? null,
|
|
782
|
+
dismissed: false,
|
|
783
|
+
createdAt: sweep.createdAt
|
|
784
|
+
}).run();
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/demo/summit-sweeps.ts
|
|
791
|
+
var SUMMIT_SWEEP_COUNT = 6;
|
|
792
|
+
var SUMMIT_ENGINES = ["openai", "gemini", "claude"];
|
|
793
|
+
var RIVALS = {
|
|
794
|
+
roofcraft: { name: "RoofCraft", domain: "roofcraft.example", path: "/roof-replacement/", blurb: "A replacement contractor known for metal and shingle installs." },
|
|
795
|
+
everlast: { name: "Everlast Roofing", domain: "everlast-roofing.example", path: "/services/", blurb: "A repair and gutter company with a large emergency crew." }
|
|
796
|
+
};
|
|
797
|
+
var SUMMIT_COMPETITORS = Object.values(RIVALS).map((rival) => ({ domain: rival.domain, label: rival.name }));
|
|
798
|
+
var SUMMIT_QUERIES = [
|
|
799
|
+
{ key: "reviews", text: "Summit Roofing reviews", pages: ["/reviews/"], rivals: [], source: "https://home-services-reviews.example/roofers/", lead: "Recent homeowner reviews are mostly positive.", blurb: "Reviewers mention clear estimates, tidy crews and quick follow-up on warranty visits." },
|
|
800
|
+
{ key: "near-me", text: "roof repair contractor near me", pages: ["/services/roof-repair/", "/service-areas/"], rivals: ["everlast", "roofcraft"], source: "https://local-contractor-directory.example/roofing/", lead: "Several roofers in the area handle repair work.", blurb: "A local roofer covering leak repairs, storm damage and inspections across two dozen towns." },
|
|
801
|
+
{ key: "emergency-leak", text: "emergency roof leak repair", pages: ["/services/roof-repair/leak-repair/", "/services/roof-repair/emergency-tarping/"], rivals: ["everlast"], source: "https://homeowner-guides.example/roof-leaks/", lead: "For an active leak, look for a crew that can tarp the roof the same day.", blurb: "Offers emergency tarping and leak repair, with same-day visits in its service area." },
|
|
802
|
+
{ key: "metal-installer", text: "best metal roof installer", pages: ["/services/roof-replacement/metal-roofing/", "/guides/metal-vs-asphalt-shingles/"], rivals: ["roofcraft"], source: "https://metal-roofing-council.example/find-an-installer/", lead: "Metal roofs need an installer with standing-seam experience.", blurb: "Installs standing-seam and metal shingle roofs and publishes a metal versus asphalt comparison." },
|
|
803
|
+
{ key: "warranty", text: "Summit Roofing warranty", pages: ["/about/warranty/"], rivals: [], source: "https://homeowner-guides.example/roof-warranties/", lead: "Roofing warranties usually combine manufacturer and workmanship coverage.", blurb: "Backs replacements with a written workmanship warranty on top of the manufacturer coverage." },
|
|
804
|
+
{ key: "replacement-cost", text: "how much does a roof replacement cost", pages: ["/guides/how-much-does-a-roof-replacement-cost/", "/financing/"], rivals: ["roofcraft"], source: "https://home-cost-report.example/roof-replacement/", lead: "Most full replacements land between 9,000 and 18,000 dollars, depending on size and material.", blurb: "Publishes a cost guide with typical price ranges and offers financing on full replacements." },
|
|
805
|
+
{ key: "storm-claims", text: "storm damage roof repair and insurance claims", pages: ["/services/roof-repair/storm-damage/", "/guides/filing-a-roof-insurance-claim/"], rivals: ["everlast"], source: "https://homeowner-guides.example/storm-claims/", lead: "Photograph the damage before any repair and call your insurer early.", blurb: "Documents storm damage for insurance claims and joins the adjuster visit." },
|
|
806
|
+
{ key: "asphalt-shingles", text: "asphalt shingle roof replacement", pages: ["/services/roof-replacement/asphalt-shingles/"], rivals: ["roofcraft"], source: "https://home-cost-report.example/asphalt-shingles/", lead: "Architectural shingles are the most common replacement choice.", blurb: "Replaces asphalt shingle roofs with a choice of architectural shingle lines." },
|
|
807
|
+
{ key: "seamless-gutters", text: "seamless gutter installation", pages: ["/services/gutters/seamless-gutters/"], rivals: ["everlast"], source: "https://homeowner-guides.example/gutters/", lead: "Seamless gutters are formed on site, so they leak less at the joints.", blurb: "Installs seamless gutters and gutter guards, often alongside a roof replacement." },
|
|
808
|
+
{ key: "pre-purchase-inspection", text: "roof inspection before buying a house", pages: ["/services/roof-inspection/real-estate-inspection/", "/guides/real-estate-roof-inspections/"], rivals: ["roofcraft", "everlast"], source: "https://home-buyer-checklist.example/roof-inspection/", lead: "A roof inspection before closing can surface repairs to negotiate.", blurb: "Runs pre-purchase roof inspections with photo reports for buyers and agents." }
|
|
809
|
+
];
|
|
810
|
+
var SUMMIT_BRANDED_QUERY_KEYS = ["reviews", "warranty"];
|
|
811
|
+
var SUMMIT_ENGINE_PLANS = {
|
|
812
|
+
// Climbs steadily.
|
|
813
|
+
openai: {
|
|
814
|
+
mentioned: [2, 3, 3, 4, 5, 6],
|
|
815
|
+
mentionOrder: ["emergency-leak", "replacement-cost", "near-me", "storm-claims", "pre-purchase-inspection", "seamless-gutters", "asphalt-shingles", "metal-installer"],
|
|
816
|
+
cited: [1, 2, 2, 3, 4, 5],
|
|
817
|
+
citationOrder: ["replacement-cost", "storm-claims", "pre-purchase-inspection", "emergency-leak", "asphalt-shingles", "metal-installer", "seamless-gutters", "near-me"]
|
|
818
|
+
},
|
|
819
|
+
// Steady, then a dip and a recovery.
|
|
820
|
+
gemini: {
|
|
821
|
+
mentioned: [4, 4, 4, 2, 3, 5],
|
|
822
|
+
mentionOrder: ["near-me", "emergency-leak", "replacement-cost", "storm-claims", "seamless-gutters", "pre-purchase-inspection", "metal-installer", "asphalt-shingles"],
|
|
823
|
+
cited: [3, 3, 3, 1, 2, 3],
|
|
824
|
+
citationOrder: ["replacement-cost", "pre-purchase-inspection", "storm-claims", "metal-installer", "emergency-leak", "asphalt-shingles", "seamless-gutters", "near-me"]
|
|
825
|
+
},
|
|
826
|
+
// Swings from week to week.
|
|
827
|
+
claude: {
|
|
828
|
+
mentioned: [3, 1, 4, 2, 4, 3],
|
|
829
|
+
mentionOrder: ["emergency-leak", "replacement-cost", "storm-claims", "near-me", "pre-purchase-inspection", "asphalt-shingles", "seamless-gutters", "metal-installer"],
|
|
830
|
+
cited: [2, 1, 2, 1, 3, 2],
|
|
831
|
+
citationOrder: ["pre-purchase-inspection", "replacement-cost", "storm-claims", "emergency-leak", "asphalt-shingles", "near-me", "metal-installer", "seamless-gutters"]
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
var BRANDED_PLAN = {
|
|
835
|
+
reviews: {
|
|
836
|
+
openai: { mentioned: [true, true, true, true, true, true], cited: [true, true, true, true, true, true] },
|
|
837
|
+
gemini: { mentioned: [true, true, true, true, true, true], cited: [true, true, true, true, true, true] },
|
|
838
|
+
claude: { mentioned: [true, true, true, true, true, true], cited: [false, false, false, false, false, false] }
|
|
839
|
+
},
|
|
840
|
+
warranty: {
|
|
841
|
+
openai: { mentioned: [true, true, true, true, true, true], cited: [true, true, true, true, true, true] },
|
|
842
|
+
gemini: { mentioned: [true, true, true, false, true, true], cited: [true, true, true, false, true, true] },
|
|
843
|
+
claude: { mentioned: [true, true, true, true, true, true], cited: [false, false, true, false, true, true] }
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
function isBranded(key) {
|
|
847
|
+
return SUMMIT_BRANDED_QUERY_KEYS.includes(key);
|
|
848
|
+
}
|
|
849
|
+
function summitAnswerPlan(sweep, engine, queryKey) {
|
|
850
|
+
if (isBranded(queryKey)) {
|
|
851
|
+
const plan2 = BRANDED_PLAN[queryKey][engine];
|
|
852
|
+
return { mentioned: plan2.mentioned[sweep], cited: plan2.cited[sweep] };
|
|
853
|
+
}
|
|
854
|
+
const plan = SUMMIT_ENGINE_PLANS[engine];
|
|
855
|
+
return {
|
|
856
|
+
mentioned: plan.mentionOrder.slice(0, plan.mentioned[sweep]).includes(queryKey),
|
|
857
|
+
cited: plan.citationOrder.slice(0, plan.cited[sweep]).includes(queryKey)
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function summitAnswer(input) {
|
|
861
|
+
const { sweep, engine, queryIndex, displayName, domain } = input;
|
|
862
|
+
const query = SUMMIT_QUERIES[queryIndex];
|
|
863
|
+
const engineIndex = SUMMIT_ENGINES.indexOf(engine);
|
|
864
|
+
const plan = summitAnswerPlan(sweep, engine, query.key);
|
|
865
|
+
const rivals = plan.mentioned ? (sweep + engineIndex + queryIndex) % 2 === 0 ? query.rivals.slice(0, 1) : [] : query.rivals;
|
|
866
|
+
const entries = [
|
|
867
|
+
...plan.mentioned ? [{ name: displayName, blurb: query.blurb }] : [],
|
|
868
|
+
...rivals.map((key) => ({ name: RIVALS[key].name, blurb: RIVALS[key].blurb }))
|
|
869
|
+
];
|
|
870
|
+
const answerText = isBranded(query.key) && !plan.mentioned ? `Fictional answer.
|
|
871
|
+
|
|
872
|
+
I could not find published details for that company. ${query.lead} Most residential roofers offer 5 to 10 years of workmanship coverage.` : [
|
|
873
|
+
"Fictional answer.",
|
|
874
|
+
query.lead,
|
|
875
|
+
entries.map((entry, index) => `${index + 1}. **${entry.name}**: ${entry.blurb}`).join("\n"),
|
|
876
|
+
"Get two or three written estimates before you commit."
|
|
877
|
+
].join("\n\n");
|
|
878
|
+
const citedRivals = rivals.filter((_, index) => (sweep + engineIndex + queryIndex + index) % 3 !== 0);
|
|
879
|
+
const citedUrls = [
|
|
880
|
+
...plan.cited ? [`https://${domain}${query.pages[(sweep + engineIndex) % query.pages.length]}`] : [],
|
|
881
|
+
...citedRivals.map((key) => `https://${RIVALS[key].domain}${RIVALS[key].path}`),
|
|
882
|
+
query.source
|
|
883
|
+
];
|
|
884
|
+
const citedDomains = citedUrls.map((url) => new URL(url).hostname);
|
|
885
|
+
const normalized = {
|
|
886
|
+
provider: engine,
|
|
887
|
+
answerText,
|
|
888
|
+
citedDomains,
|
|
889
|
+
groundingSources: citedUrls.map((uri) => ({ uri, title: "" })),
|
|
890
|
+
searchQueries: [query.text],
|
|
891
|
+
retrievalStatus: "used"
|
|
892
|
+
};
|
|
893
|
+
const domains = effectiveDomains({ canonicalDomain: domain, ownedDomains: [domain] });
|
|
894
|
+
const brandNames = effectiveBrandNames({ displayName, canonicalDomain: domain, ownedDomains: [domain] });
|
|
895
|
+
const competitorDomains = SUMMIT_COMPETITORS.map((competitor) => competitor.domain);
|
|
896
|
+
return {
|
|
897
|
+
answerText,
|
|
898
|
+
answerMentioned: determineAnswerMentioned(answerText, brandNames, domains),
|
|
899
|
+
citationState: determineCitationState(normalized, domains),
|
|
900
|
+
citedDomains,
|
|
901
|
+
citedUrls,
|
|
902
|
+
competitorOverlap: computeCompetitorOverlap(normalized, competitorDomains),
|
|
903
|
+
recommendedCompetitors: extractRecommendedCompetitors(answerText, domains, citedDomains, competitorDomains, brandNames)
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/demo/seed-core.ts
|
|
908
|
+
var PROVIDERS = [
|
|
909
|
+
{ provider: "openai", requestedModel: "gpt-5-demo", servedModel: "gpt-5-demo-2026-08-15" },
|
|
910
|
+
{ provider: "gemini", requestedModel: "gemini-2.5-pro-demo", servedModel: "gemini-2.5-pro" },
|
|
911
|
+
{ provider: "claude", requestedModel: "claude-sonnet-demo", servedModel: "claude-sonnet-4-demo" }
|
|
912
|
+
];
|
|
913
|
+
function isoAtWeek(now, weeksAgo) {
|
|
914
|
+
return new Date(now.getTime() - weeksAgo * 7 * 24 * 60 * 60 * 1e3).toISOString();
|
|
915
|
+
}
|
|
916
|
+
function sha256(value) {
|
|
917
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
918
|
+
}
|
|
919
|
+
function harborLocations() {
|
|
920
|
+
return HARBOR_MARKETS.map((market) => ({
|
|
921
|
+
label: market.label,
|
|
922
|
+
city: market.city,
|
|
923
|
+
region: market.region,
|
|
924
|
+
country: "US"
|
|
925
|
+
}));
|
|
926
|
+
}
|
|
927
|
+
function harborPlan(context, capturedAt) {
|
|
928
|
+
const properties = harborProperties();
|
|
929
|
+
const locations = new Map(harborLocations().map((location) => [location.label, location]));
|
|
930
|
+
const querySnapshots2 = properties.flatMap((property) => [
|
|
931
|
+
{ queryId: `${property.key}-brand`, queryText: `${property.label} reviews` },
|
|
932
|
+
{ queryId: `${property.key}-nonbrand`, queryText: `best luxury resort in ${property.market.label} for ${["oceanfront pools", "family suites", "spa weekends", "sunset dining"][property.number - 1]}` }
|
|
933
|
+
].map((query) => ({ ...query, provenance: { source: "manual", sourceId: "demo-core", capturedAt } })));
|
|
934
|
+
const assignments = properties.flatMap((property) => [
|
|
935
|
+
{ suffix: "brand", queryClass: "branded" },
|
|
936
|
+
{ suffix: "nonbrand", queryClass: "non-brand" }
|
|
937
|
+
].map((item) => ({
|
|
938
|
+
targetKey: property.key,
|
|
939
|
+
queryId: `${property.key}-${item.suffix}`,
|
|
940
|
+
queryClass: item.queryClass,
|
|
941
|
+
classificationSource: "operator",
|
|
942
|
+
executionNodeKey: `exec-${property.key}-${item.suffix}`
|
|
943
|
+
})));
|
|
944
|
+
const executionNodes = assignments.map((assignment) => {
|
|
945
|
+
const property = properties.find((candidate) => candidate.key === assignment.targetKey);
|
|
946
|
+
const query = querySnapshots2.find((candidate) => candidate.queryId === assignment.queryId);
|
|
947
|
+
return {
|
|
948
|
+
stableKey: assignment.executionNodeKey,
|
|
949
|
+
queryId: query.queryId,
|
|
950
|
+
queryText: query.queryText,
|
|
951
|
+
context: {
|
|
952
|
+
providers: PROVIDERS.map((identity) => identity.provider),
|
|
953
|
+
models: Object.fromEntries(PROVIDERS.map((identity) => [identity.provider, identity.requestedModel])),
|
|
954
|
+
location: locations.get(property.market.label)
|
|
955
|
+
},
|
|
956
|
+
expectedSnapshots: PROVIDERS.length
|
|
957
|
+
};
|
|
958
|
+
});
|
|
959
|
+
const usageEdges = assignments.map((assignment) => ({
|
|
960
|
+
executionNodeKey: assignment.executionNodeKey,
|
|
961
|
+
targetKey: assignment.targetKey,
|
|
962
|
+
queryId: assignment.queryId
|
|
963
|
+
}));
|
|
964
|
+
const provisional = {
|
|
965
|
+
schemaVersion: 2,
|
|
966
|
+
identities: {
|
|
967
|
+
projectBrand: {
|
|
968
|
+
canonicalHost: context.portfolio.domain,
|
|
969
|
+
ownedHosts: [context.portfolio.domain],
|
|
970
|
+
names: ["Harbor Resorts", "Harbor"]
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
targets: properties.map((property) => ({
|
|
974
|
+
stableKey: property.key,
|
|
975
|
+
label: property.label,
|
|
976
|
+
aliases: [property.label, `Harbor ${property.market.label}`],
|
|
977
|
+
urlMatchers: [{ kind: "prefix", host: context.portfolio.domain, pathPrefix: property.path, pathCase: "insensitive" }],
|
|
978
|
+
mentionNotApplicable: false,
|
|
979
|
+
discoveryIdentity: `demo:${property.key}`
|
|
980
|
+
})),
|
|
981
|
+
groups: HARBOR_MARKETS.map((market) => ({
|
|
982
|
+
stableKey: `market-${market.key}`,
|
|
983
|
+
label: `${market.label} market`,
|
|
984
|
+
targetKeys: properties.filter((property) => property.market.key === market.key).map((property) => property.key),
|
|
985
|
+
competitors: [
|
|
986
|
+
{ stableKey: "seaside-collection", label: "Seaside Collection", domain: "seaside-collection.example", aliases: ["Seaside Collection"] },
|
|
987
|
+
{ stableKey: "coastal-stays", label: "Coastal Stays", domain: "coastal-stays.example", aliases: ["Coastal Stays"] }
|
|
988
|
+
]
|
|
989
|
+
})),
|
|
990
|
+
querySnapshots: querySnapshots2,
|
|
991
|
+
assignments,
|
|
992
|
+
executionNodes,
|
|
993
|
+
usageEdges,
|
|
994
|
+
reportingScopes: HARBOR_MARKETS.map((market) => ({
|
|
995
|
+
stableKey: `market-${market.key}`,
|
|
996
|
+
label: `${market.label} market`,
|
|
997
|
+
kind: "market",
|
|
998
|
+
usageEdges: usageEdges.filter((edge) => edge.targetKey.includes(market.key))
|
|
999
|
+
})),
|
|
1000
|
+
compiledChecksum: "0".repeat(64)
|
|
1001
|
+
};
|
|
1002
|
+
const parsed = measurementPlanV2Schema.parse(provisional);
|
|
1003
|
+
return measurementPlanV2Schema.parse({
|
|
1004
|
+
...parsed,
|
|
1005
|
+
compiledChecksum: sha256(measurementPlanV2ChecksumJson(parsed))
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
function harborDraft(plan) {
|
|
1009
|
+
const targets = plan.targets.map((target) => ({
|
|
1010
|
+
stableKey: target.stableKey,
|
|
1011
|
+
label: target.label,
|
|
1012
|
+
status: "included",
|
|
1013
|
+
aliases: target.aliases,
|
|
1014
|
+
urlMatchers: target.urlMatchers.map((matcher) => matcher.kind === "prefix" ? `https://${matcher.host}${matcher.pathPrefix}/*` : matcher.kind === "host" ? matcher.host : matcher.url),
|
|
1015
|
+
source: "sitemap",
|
|
1016
|
+
discoveredUrl: `https://${target.urlMatchers[0].kind === "prefix" ? target.urlMatchers[0].host + target.urlMatchers[0].pathPrefix : "harbor-resorts.example"}`,
|
|
1017
|
+
discoveryIdentity: target.discoveryIdentity ?? void 0
|
|
1018
|
+
}));
|
|
1019
|
+
return {
|
|
1020
|
+
defaultContext: { providers: PROVIDERS.map((identity) => identity.provider), models: Object.fromEntries(PROVIDERS.map((identity) => [identity.provider, identity.requestedModel])), locations: HARBOR_MARKETS.map((market) => market.label) },
|
|
1021
|
+
targets,
|
|
1022
|
+
assignments: plan.assignments.map((assignment) => ({
|
|
1023
|
+
targetKey: assignment.targetKey,
|
|
1024
|
+
queryId: assignment.queryId,
|
|
1025
|
+
queryClass: assignment.queryClass,
|
|
1026
|
+
classificationSource: "operator",
|
|
1027
|
+
executionContexts: [plan.executionNodes.find((node) => node.stableKey === assignment.executionNodeKey).context]
|
|
1028
|
+
})),
|
|
1029
|
+
groups: plan.groups.map((group) => ({
|
|
1030
|
+
stableKey: group.stableKey,
|
|
1031
|
+
label: group.label,
|
|
1032
|
+
targetKeys: group.targetKeys,
|
|
1033
|
+
competitors: group.competitors
|
|
1034
|
+
})),
|
|
1035
|
+
reportingScopes: plan.reportingScopes
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function seedDemoCore(db, context) {
|
|
1039
|
+
const now = context.now.toISOString();
|
|
1040
|
+
const simpleQueries = SUMMIT_QUERIES.map((plan, index) => ({ id: `demo-summit-query-${index + 1}`, query: plan.text }));
|
|
1041
|
+
const portfolioPlan = harborPlan(context, now);
|
|
1042
|
+
const planVersionId = "demo-harbor-plan-v2";
|
|
1043
|
+
const execution = { providers: PROVIDERS.map((identity) => identity.provider), models: Object.fromEntries(PROVIDERS.map((identity) => [identity.provider, identity.requestedModel])) };
|
|
1044
|
+
const executionIdentity = buildMeasurementExecutionIdentity(execution, sha256(canonicalMeasurementExecutionIdentityJson(execution)));
|
|
1045
|
+
const planManifest = buildMeasurementRunManifestV1({
|
|
1046
|
+
expectedSlots: portfolioPlan.executionNodes.flatMap((node) => node.context.providers.map((provider) => ({
|
|
1047
|
+
executionId: node.stableKey,
|
|
1048
|
+
queryText: node.queryText,
|
|
1049
|
+
provider,
|
|
1050
|
+
context: node.context.location,
|
|
1051
|
+
requestedModel: node.context.models[provider]
|
|
1052
|
+
})))
|
|
1053
|
+
});
|
|
1054
|
+
db.transaction((tx) => {
|
|
1055
|
+
tx.insert(projects).values([
|
|
1056
|
+
{
|
|
1057
|
+
id: context.simple.id,
|
|
1058
|
+
name: context.simple.name,
|
|
1059
|
+
displayName: context.simple.displayName,
|
|
1060
|
+
canonicalDomain: context.simple.domain,
|
|
1061
|
+
ownedDomains: [context.simple.domain],
|
|
1062
|
+
country: "US",
|
|
1063
|
+
language: "en",
|
|
1064
|
+
providers: PROVIDERS.map((identity) => identity.provider),
|
|
1065
|
+
providerModels: Object.fromEntries(PROVIDERS.map((identity) => [identity.provider, identity.requestedModel])),
|
|
1066
|
+
createdAt: now,
|
|
1067
|
+
updatedAt: now
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
id: context.portfolio.id,
|
|
1071
|
+
name: context.portfolio.name,
|
|
1072
|
+
displayName: context.portfolio.displayName,
|
|
1073
|
+
canonicalDomain: context.portfolio.domain,
|
|
1074
|
+
ownedDomains: [context.portfolio.domain],
|
|
1075
|
+
country: "US",
|
|
1076
|
+
language: "en",
|
|
1077
|
+
providers: PROVIDERS.map((identity) => identity.provider),
|
|
1078
|
+
providerModels: Object.fromEntries(PROVIDERS.map((identity) => [identity.provider, identity.requestedModel])),
|
|
1079
|
+
locations: harborLocations(),
|
|
1080
|
+
defaultLocation: "Key West",
|
|
1081
|
+
createdAt: now,
|
|
1082
|
+
updatedAt: now
|
|
1083
|
+
}
|
|
1084
|
+
]).run();
|
|
1085
|
+
tx.insert(queries).values([
|
|
1086
|
+
...simpleQueries.map((item) => ({ id: item.id, projectId: context.simple.id, query: item.query, provenance: "demo-core", createdAt: now })),
|
|
1087
|
+
...portfolioPlan.querySnapshots.map((snapshot) => ({ id: snapshot.queryId, projectId: context.portfolio.id, query: snapshot.queryText, provenance: "demo-core", createdAt: now }))
|
|
1088
|
+
]).run();
|
|
1089
|
+
tx.insert(competitors).values([
|
|
1090
|
+
{ id: "demo-summit-competitor-1", projectId: context.simple.id, domain: "roofcraft.example", provenance: "demo-core", createdAt: now },
|
|
1091
|
+
{ id: "demo-summit-competitor-2", projectId: context.simple.id, domain: "everlast-roofing.example", provenance: "demo-core", createdAt: now },
|
|
1092
|
+
{ id: "demo-harbor-competitor-1", projectId: context.portfolio.id, domain: "seaside-collection.example", provenance: "demo-core", createdAt: now },
|
|
1093
|
+
{ id: "demo-harbor-competitor-2", projectId: context.portfolio.id, domain: "coastal-stays.example", provenance: "demo-core", createdAt: now }
|
|
1094
|
+
]).run();
|
|
1095
|
+
tx.insert(measurementPlanVersions).values({
|
|
1096
|
+
id: planVersionId,
|
|
1097
|
+
projectId: context.portfolio.id,
|
|
1098
|
+
revision: 2,
|
|
1099
|
+
canonicalJson: canonicalMeasurementPlanV2Json(portfolioPlan),
|
|
1100
|
+
checksum: sha256(canonicalMeasurementPlanV2Json(portfolioPlan)),
|
|
1101
|
+
schemaVersion: 2,
|
|
1102
|
+
compiledChecksum: portfolioPlan.compiledChecksum,
|
|
1103
|
+
publishedBy: "demo-reviewed",
|
|
1104
|
+
createdAt: now
|
|
1105
|
+
}).run();
|
|
1106
|
+
tx.insert(measurementPlans).values({ projectId: context.portfolio.id, activeVersionId: planVersionId, createdAt: now, updatedAt: now }).run();
|
|
1107
|
+
tx.insert(measurementSegments).values([
|
|
1108
|
+
...portfolioPlan.targets.map((target) => ({ id: `segment-${target.stableKey}`, projectId: context.portfolio.id, stableKey: target.stableKey, kind: "target", createdAt: now })),
|
|
1109
|
+
...portfolioPlan.groups.map((group) => ({ id: `segment-${group.stableKey}`, projectId: context.portfolio.id, stableKey: group.stableKey, kind: "group", createdAt: now }))
|
|
1110
|
+
]).run();
|
|
1111
|
+
tx.insert(measurementPlanDrafts).values({
|
|
1112
|
+
id: "demo-harbor-authoring-draft",
|
|
1113
|
+
projectId: context.portfolio.id,
|
|
1114
|
+
schemaVersion: 2,
|
|
1115
|
+
baseActiveVersionId: planVersionId,
|
|
1116
|
+
baseActiveRevision: 2,
|
|
1117
|
+
authoringJson: JSON.stringify(harborDraft(portfolioPlan)),
|
|
1118
|
+
etagVersion: 7,
|
|
1119
|
+
createdBy: JSON.stringify({ kind: "system", id: "demo", label: "Demo authoring" }),
|
|
1120
|
+
updatedBy: JSON.stringify({ kind: "system", id: "demo", label: "Demo authoring" }),
|
|
1121
|
+
createdAt: now,
|
|
1122
|
+
updatedAt: now
|
|
1123
|
+
}).run();
|
|
1124
|
+
for (let week = SUMMIT_SWEEP_COUNT - 1; week >= 0; week--) {
|
|
1125
|
+
const createdAt = isoAtWeek(context.now, week);
|
|
1126
|
+
const sweep = SUMMIT_SWEEP_COUNT - 1 - week;
|
|
1127
|
+
const simpleRunId = `demo-summit-week-${sweep + 1}`;
|
|
1128
|
+
const simpleDefinition = buildSimpleMeasurementDefinition({
|
|
1129
|
+
capturedAt: createdAt,
|
|
1130
|
+
identity: { displayName: context.simple.displayName, aliases: ["Summit", "Summit Roofing"], canonicalDomain: context.simple.domain, ownedDomains: [context.simple.domain] },
|
|
1131
|
+
country: "US",
|
|
1132
|
+
language: "en",
|
|
1133
|
+
location: null,
|
|
1134
|
+
engines: PROVIDERS.map((identity) => ({ provider: identity.provider, requestedModel: identity.requestedModel })),
|
|
1135
|
+
competitors: [
|
|
1136
|
+
{ domain: SUMMIT_COMPETITORS[0].domain, label: SUMMIT_COMPETITORS[0].label, aliases: ["RoofCraft"] },
|
|
1137
|
+
{ domain: SUMMIT_COMPETITORS[1].domain, label: SUMMIT_COMPETITORS[1].label, aliases: ["Everlast"] }
|
|
1138
|
+
],
|
|
1139
|
+
queries: simpleQueries.map((item) => ({ queryId: item.id, queryText: item.query, provenance: "demo-core" }))
|
|
1140
|
+
});
|
|
1141
|
+
tx.insert(runs).values({ id: simpleRunId, projectId: context.simple.id, kind: "answer-visibility", status: "completed", trigger: "manual", queries: simpleQueries.map((item) => item.query), startedAt: createdAt, finishedAt: createdAt, createdAt }).run();
|
|
1142
|
+
tx.insert(simpleMeasurementDefinitions).values({
|
|
1143
|
+
runId: simpleRunId,
|
|
1144
|
+
projectId: context.simple.id,
|
|
1145
|
+
definition: simpleDefinition,
|
|
1146
|
+
checksum: sha256(canonicalSimpleMeasurementDefinitionJson(simpleDefinition)),
|
|
1147
|
+
capturedAt: createdAt
|
|
1148
|
+
}).run();
|
|
1149
|
+
tx.insert(querySnapshots).values(simpleQueries.flatMap((query, queryIndex) => PROVIDERS.map((identity) => {
|
|
1150
|
+
const engine = SUMMIT_ENGINES.find((candidate) => candidate === identity.provider);
|
|
1151
|
+
const answer = summitAnswer({ sweep, engine, queryIndex, displayName: context.simple.displayName, domain: context.simple.domain });
|
|
1152
|
+
return {
|
|
1153
|
+
id: `${simpleRunId}-${identity.provider}-${queryIndex}`,
|
|
1154
|
+
runId: simpleRunId,
|
|
1155
|
+
queryId: query.id,
|
|
1156
|
+
queryText: query.query,
|
|
1157
|
+
provider: identity.provider,
|
|
1158
|
+
model: identity.requestedModel,
|
|
1159
|
+
servedModel: identity.servedModel,
|
|
1160
|
+
...answer,
|
|
1161
|
+
captureStatus: "complete",
|
|
1162
|
+
sourceCount: answer.citedUrls.length,
|
|
1163
|
+
resolvedCount: answer.citedUrls.length,
|
|
1164
|
+
captureVersion: 1,
|
|
1165
|
+
retrievalStatus: "used",
|
|
1166
|
+
createdAt
|
|
1167
|
+
};
|
|
1168
|
+
}))).run();
|
|
1169
|
+
const portfolioRunId = `demo-harbor-week-${6 - week}`;
|
|
1170
|
+
tx.insert(runs).values({
|
|
1171
|
+
id: portfolioRunId,
|
|
1172
|
+
projectId: context.portfolio.id,
|
|
1173
|
+
kind: "answer-visibility",
|
|
1174
|
+
status: "completed",
|
|
1175
|
+
trigger: "manual",
|
|
1176
|
+
measurementPlanVersionId: planVersionId,
|
|
1177
|
+
measurementManifest: planManifest,
|
|
1178
|
+
measurementExecutionIdentity: executionIdentity,
|
|
1179
|
+
startedAt: createdAt,
|
|
1180
|
+
finishedAt: createdAt,
|
|
1181
|
+
createdAt
|
|
1182
|
+
}).run();
|
|
1183
|
+
tx.insert(querySnapshots).values(portfolioPlan.executionNodes.flatMap((node, nodeIndex) => PROVIDERS.map((identity, providerIndex) => {
|
|
1184
|
+
const target = portfolioPlan.targets.find((candidate) => node.stableKey.includes(candidate.stableKey));
|
|
1185
|
+
const cited = !(week === 0 && identity.provider === "gemini" && node.stableKey.endsWith("nonbrand")) && (week + nodeIndex + providerIndex) % 4 !== 0;
|
|
1186
|
+
const competitor = providerIndex === 1 ? "coastal-stays.example" : "seaside-collection.example";
|
|
1187
|
+
return {
|
|
1188
|
+
id: `${portfolioRunId}-${node.stableKey}-${identity.provider}`,
|
|
1189
|
+
runId: portfolioRunId,
|
|
1190
|
+
queryId: node.queryId,
|
|
1191
|
+
queryText: node.queryText,
|
|
1192
|
+
provider: identity.provider,
|
|
1193
|
+
model: identity.requestedModel,
|
|
1194
|
+
servedModel: identity.servedModel,
|
|
1195
|
+
citationState: cited ? "cited" : "not-cited",
|
|
1196
|
+
answerMentioned: cited,
|
|
1197
|
+
answerText: cited ? `Fictional answer: ${target.label} is a Harbor Resorts option for this stay.` : `Fictional answer: ${providerIndex === 1 ? "Coastal Stays" : "Seaside Collection"} is recommended instead.`,
|
|
1198
|
+
citedDomains: cited ? [context.portfolio.domain] : [competitor],
|
|
1199
|
+
citedUrls: cited ? [`https://${context.portfolio.domain}${target.urlMatchers[0].kind === "prefix" ? target.urlMatchers[0].pathPrefix : "/"}`] : [`https://${competitor}/guides/luxury-stays`],
|
|
1200
|
+
captureStatus: "complete",
|
|
1201
|
+
sourceCount: 1,
|
|
1202
|
+
resolvedCount: 1,
|
|
1203
|
+
captureVersion: 1,
|
|
1204
|
+
retrievalStatus: "used",
|
|
1205
|
+
competitorOverlap: cited ? [] : [competitor],
|
|
1206
|
+
recommendedCompetitors: [competitor],
|
|
1207
|
+
measurementExecutionId: node.stableKey,
|
|
1208
|
+
requestedContext: node.context.location,
|
|
1209
|
+
supportedContext: { status: "applied", resolved: node.context.location },
|
|
1210
|
+
location: node.context.location.label,
|
|
1211
|
+
createdAt
|
|
1212
|
+
};
|
|
1213
|
+
}))).run();
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
seedDemoAnswerIntelligence(db, context.simple);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/demo/seed-site-crawl.ts
|
|
1220
|
+
var DEMO_AUDIT_FACTORS = [
|
|
1221
|
+
{ id: "structured-data", name: "Structured Data (JSON-LD)", weight: 12, bias: 4, found: "JSON-LD markup describes this page.", missing: "JSON-LD markup is missing or incomplete.", recommendation: "Add JSON-LD that describes the business and this page." },
|
|
1222
|
+
{ id: "content-depth", name: "Content Depth", weight: 10, bias: 0, found: "The page covers its topic in depth.", missing: "The page has little original text.", recommendation: "Expand the page with specific details and examples." },
|
|
1223
|
+
{ id: "faq-content", name: "FAQ Content", weight: 8, bias: -10, found: "The page has an FAQ section with direct answers.", missing: "No FAQ content was found.", recommendation: "Add an FAQ section that answers what visitors ask about this topic." },
|
|
1224
|
+
{ id: "schema-completeness", name: "Schema Completeness", weight: 8, bias: -4, found: "Schema properties cover the key details.", missing: "Key schema properties are missing.", recommendation: "Fill in the missing schema properties." },
|
|
1225
|
+
{ id: "eeat-signals", name: "E-E-A-T Signals", weight: 8, bias: 2, found: "Author, credentials, and business details are visible.", missing: "No author or credentials are shown.", recommendation: "Name the author and show relevant credentials." },
|
|
1226
|
+
{ id: "citations", name: "Citations & Authority Signals", weight: 8, bias: -8, found: "The page cites supporting sources.", missing: "The page makes claims without supporting sources.", recommendation: "Link to the sources that support the claims on this page." },
|
|
1227
|
+
{ id: "lighthouse", name: "Lighthouse (Performance/A11y/Best Practices)", weight: 8, bias: 3, found: "Performance and accessibility checks pass.", missing: "Performance or accessibility checks need work.", recommendation: "Compress large images and fix the flagged accessibility issues." },
|
|
1228
|
+
{ id: "geographic-signals", name: "Geographic Signals", weight: 7, bias: 2, found: "The places this business serves are named on the page.", missing: "The page does not say where the business operates.", recommendation: "Name the towns and regions this page serves." },
|
|
1229
|
+
{ id: "content-freshness", name: "Content Freshness", weight: 7, bias: -3, found: "The page shows a recent update date.", missing: "No recent update date was found.", recommendation: "Review the page and show when it was last updated." },
|
|
1230
|
+
{ id: "entity-consistency", name: "Entity Consistency", weight: 7, bias: 6, found: "Business name and contact details match across the site.", missing: "Business name or contact details differ from other pages.", recommendation: "Use the same business name, phone, and address everywhere." },
|
|
1231
|
+
{ id: "agent-skill-exposure", name: "Agent Skill Exposure", weight: 6, bias: -12, found: "Key actions are described in a machine-readable form.", missing: "No machine-readable actions were found.", recommendation: "Describe key actions, such as booking or requesting a quote, in a machine-readable form." },
|
|
1232
|
+
{ id: "content-extractability", name: "Content Extractability", weight: 6, bias: 5, found: "Main content is plain HTML that is easy to extract.", missing: "Main content is hard to extract from the page markup.", recommendation: "Keep the main content in plain HTML rather than scripts or images." },
|
|
1233
|
+
{ id: "definition-blocks", name: "Definition Blocks", weight: 6, bias: -6, found: "Key terms are defined in short, direct passages.", missing: "Key terms are not defined directly.", recommendation: "Add a short, direct definition of the main topic." },
|
|
1234
|
+
{ id: "named-entities", name: "Named Entities", weight: 6, bias: 1, found: "Products, places, and organizations are named specifically.", missing: "Few specific products, places, or organizations are named.", recommendation: "Name the specific products, places, and organizations involved." },
|
|
1235
|
+
{ id: "snippet-eligibility", name: "Snippet Eligibility", weight: 6, bias: -2, found: "A concise answer passage is available.", missing: "No concise answer passage was found.", recommendation: "Open with a two or three sentence answer to the main query." },
|
|
1236
|
+
{ id: "ai-access-files", name: "AI Access Files (llms.txt, sitemap)", weight: 5, bias: 8, found: "llms.txt and the sitemap are available.", missing: "AI access files are missing or incomplete.", recommendation: "Publish llms.txt and keep the sitemap current." },
|
|
1237
|
+
{ id: "schema-validity", name: "Schema Validity", weight: 5, bias: 7, found: "Structured data parses without errors.", missing: "Structured data has validation errors.", recommendation: "Fix the invalid structured data fields." },
|
|
1238
|
+
{ id: "technical-seo", name: "Technical SEO", weight: 5, bias: 6, found: "Title, description, and headings are in place.", missing: "The title or meta description needs attention.", recommendation: "Write a unique title and meta description." },
|
|
1239
|
+
{ id: "ai-crawler-access", name: "AI Crawler Access", weight: 4, bias: 9, found: "AI crawlers are allowed to fetch this page.", missing: "Some AI crawlers are blocked from this page.", recommendation: "Review the robots.txt rules for AI crawlers." }
|
|
1240
|
+
];
|
|
1241
|
+
var ROOT_PATH = "/";
|
|
1242
|
+
var BROKEN_STATUS = 404;
|
|
1243
|
+
var HTML_KINDS = /* @__PURE__ */ new Set(["html", "noindex", "canonicalized"]);
|
|
1244
|
+
function unitHash(value) {
|
|
1245
|
+
let hash = 2166136261;
|
|
1246
|
+
for (let index = 0; index < value.length; index++) {
|
|
1247
|
+
hash ^= value.charCodeAt(index);
|
|
1248
|
+
hash = Math.imul(hash, 16777619);
|
|
1249
|
+
}
|
|
1250
|
+
return (hash >>> 0) / 4294967296;
|
|
1251
|
+
}
|
|
1252
|
+
function anchorText(path) {
|
|
1253
|
+
if (path === ROOT_PATH) return "Home";
|
|
1254
|
+
const segment = path.split("/").filter(Boolean).at(-1) ?? path;
|
|
1255
|
+
if (segment.endsWith(".pdf")) return `${anchorText(`/${segment.slice(0, -4)}/`)} (PDF)`;
|
|
1256
|
+
if (segment.includes(".")) return segment;
|
|
1257
|
+
const words = segment.replaceAll("-", " ");
|
|
1258
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
1259
|
+
}
|
|
1260
|
+
function parentPath(path) {
|
|
1261
|
+
if (path === ROOT_PATH) return ROOT_PATH;
|
|
1262
|
+
const trimmed = path.endsWith("/") ? path.slice(0, -1) : path;
|
|
1263
|
+
const slash = trimmed.lastIndexOf("/");
|
|
1264
|
+
return slash <= 0 ? ROOT_PATH : `${trimmed.slice(0, slash)}/`;
|
|
1265
|
+
}
|
|
1266
|
+
function auditFactors(page) {
|
|
1267
|
+
const totalWeight = DEMO_AUDIT_FACTORS.reduce((sum, factor) => sum + factor.weight, 0);
|
|
1268
|
+
const offsets = DEMO_AUDIT_FACTORS.map((factor) => factor.bias + (unitHash(`${page.path}#${factor.id}`) * 2 - 1) * 12);
|
|
1269
|
+
const meanOffset = DEMO_AUDIT_FACTORS.reduce((sum, factor, index) => sum + offsets[index] * factor.weight, 0) / totalWeight;
|
|
1270
|
+
const factors = DEMO_AUDIT_FACTORS.map((factor, index) => {
|
|
1271
|
+
const score2 = page.factorScores?.[factor.id] ?? Math.max(0, Math.min(100, Math.round(page.score + offsets[index] - meanOffset)));
|
|
1272
|
+
const status = factorStatusFromScore(score2);
|
|
1273
|
+
const passing = status === "pass";
|
|
1274
|
+
return {
|
|
1275
|
+
id: factor.id,
|
|
1276
|
+
name: factor.name,
|
|
1277
|
+
weight: factor.weight,
|
|
1278
|
+
score: score2,
|
|
1279
|
+
status,
|
|
1280
|
+
applicable: true,
|
|
1281
|
+
findings: [{ type: passing ? "found" : "missing", code: `${factor.id}.${passing ? "present" : "gap"}`, message: passing ? factor.found : factor.missing }],
|
|
1282
|
+
recommendations: passing ? [] : [factor.recommendation]
|
|
1283
|
+
};
|
|
1284
|
+
});
|
|
1285
|
+
const score = Math.round(factors.reduce((sum, factor) => sum + factor.score * factor.weight, 0) / totalWeight);
|
|
1286
|
+
return { score, factors };
|
|
1287
|
+
}
|
|
1288
|
+
function buildDemoSiteCrawl(input) {
|
|
1289
|
+
const { inventory, root, prefix, nowIso } = input;
|
|
1290
|
+
const scope = { projectId: input.project.id, runId: input.crawlRunId, attemptId: input.attemptId };
|
|
1291
|
+
const url = (path) => new URL(path, root).href;
|
|
1292
|
+
const sitemapUrl = url("/sitemap.xml");
|
|
1293
|
+
const pagesByPath = /* @__PURE__ */ new Map();
|
|
1294
|
+
for (const page of inventory.pages) {
|
|
1295
|
+
if (pagesByPath.has(page.path)) throw new Error(`Demo site inventory lists ${page.path} more than once`);
|
|
1296
|
+
pagesByPath.set(page.path, page);
|
|
1297
|
+
}
|
|
1298
|
+
if (!pagesByPath.has(ROOT_PATH)) throw new Error("Demo site inventory has no root page");
|
|
1299
|
+
const requirePage = (path, role) => {
|
|
1300
|
+
if (!pagesByPath.has(path)) throw new Error(`Demo site ${role} ${path} is not a page in the inventory`);
|
|
1301
|
+
};
|
|
1302
|
+
const requireIndexable = (from, relation, target) => {
|
|
1303
|
+
if (pagesByPath.get(target)?.kind !== "html") {
|
|
1304
|
+
throw new Error(`Demo site page ${from} ${relation} ${target}, which is not an indexable HTML page`);
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
for (const page of inventory.pages) {
|
|
1308
|
+
if (page.kind === "redirect") requireIndexable(page.path, "redirects to", page.redirectTo);
|
|
1309
|
+
if (page.kind === "canonicalized") requireIndexable(page.path, "declares its canonical as", page.canonicalTo);
|
|
1310
|
+
}
|
|
1311
|
+
const drafts = /* @__PURE__ */ new Map();
|
|
1312
|
+
const observe = (source, target, placement) => {
|
|
1313
|
+
requirePage(source, "link source");
|
|
1314
|
+
requirePage(target, "link target");
|
|
1315
|
+
if (!HTML_KINDS.has(pagesByPath.get(source).kind)) {
|
|
1316
|
+
throw new Error(`Demo site link source ${source} is not an HTML page, so no crawl could record links from it`);
|
|
1317
|
+
}
|
|
1318
|
+
if (source === target) return;
|
|
1319
|
+
const key = `${source}->${target}`;
|
|
1320
|
+
let draft = drafts.get(key);
|
|
1321
|
+
if (!draft) {
|
|
1322
|
+
draft = { source, target, navigation: 0, content: 0, anchors: /* @__PURE__ */ new Set() };
|
|
1323
|
+
drafts.set(key, draft);
|
|
1324
|
+
}
|
|
1325
|
+
draft[placement]++;
|
|
1326
|
+
draft.anchors.add(anchorText(target));
|
|
1327
|
+
};
|
|
1328
|
+
for (const target of inventory.templateLinks) requirePage(target, "menu or footer link target");
|
|
1329
|
+
for (const page of inventory.pages) {
|
|
1330
|
+
if (!HTML_KINDS.has(page.kind)) continue;
|
|
1331
|
+
for (const target of inventory.templateLinks) observe(page.path, target, "navigation");
|
|
1332
|
+
}
|
|
1333
|
+
for (const [source, target, placement = "content"] of inventory.links) observe(source, target, placement);
|
|
1334
|
+
const edges = [...drafts.entries()].map(([edgeKey, draft]) => {
|
|
1335
|
+
const occurrences = draft.navigation + draft.content;
|
|
1336
|
+
return {
|
|
1337
|
+
...scope,
|
|
1338
|
+
id: `${prefix}-edge-${edgeKey}`,
|
|
1339
|
+
edgeKey,
|
|
1340
|
+
sourceNodeKey: draft.source,
|
|
1341
|
+
sourceUrl: url(draft.source),
|
|
1342
|
+
targetNodeKey: draft.target,
|
|
1343
|
+
targetUrl: url(draft.target),
|
|
1344
|
+
relation: "anchor",
|
|
1345
|
+
internal: true,
|
|
1346
|
+
followable: true,
|
|
1347
|
+
occurrences,
|
|
1348
|
+
followableOccurrences: occurrences,
|
|
1349
|
+
nofollowOccurrences: 0,
|
|
1350
|
+
anchors: [...draft.anchors],
|
|
1351
|
+
isTemplate: placementLinkDecision({ navigation: draft.navigation, content: draft.content, unknown: 0 }) === "navigation",
|
|
1352
|
+
templateRatio: null,
|
|
1353
|
+
placementNavigationOccurrences: draft.navigation,
|
|
1354
|
+
placementContentOccurrences: draft.content,
|
|
1355
|
+
placementUnknownOccurrences: 0,
|
|
1356
|
+
createdAt: nowIso,
|
|
1357
|
+
updatedAt: nowIso
|
|
1358
|
+
};
|
|
1359
|
+
});
|
|
1360
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
1361
|
+
const outbound = /* @__PURE__ */ new Map();
|
|
1362
|
+
for (const edge of edges) {
|
|
1363
|
+
inbound.set(edge.targetNodeKey, [...inbound.get(edge.targetNodeKey) ?? [], edge]);
|
|
1364
|
+
outbound.set(edge.sourceNodeKey, [...outbound.get(edge.sourceNodeKey) ?? [], edge]);
|
|
1365
|
+
}
|
|
1366
|
+
const inSitemap = (page) => page.sitemap ?? page.kind === "html";
|
|
1367
|
+
for (const page of inventory.pages) {
|
|
1368
|
+
if (page.path !== ROOT_PATH && !inbound.has(page.path) && !inSitemap(page)) {
|
|
1369
|
+
throw new Error(`Demo site page ${page.path} has no inbound link and is not in the sitemap, so no crawl could find it`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
const depths = /* @__PURE__ */ new Map([[ROOT_PATH, 0]]);
|
|
1373
|
+
for (const queue = [ROOT_PATH]; queue.length > 0; ) {
|
|
1374
|
+
const from = queue.shift();
|
|
1375
|
+
for (const edge of outbound.get(from) ?? []) {
|
|
1376
|
+
if (depths.has(edge.targetNodeKey)) continue;
|
|
1377
|
+
depths.set(edge.targetNodeKey, depths.get(from) + 1);
|
|
1378
|
+
queue.push(edge.targetNodeKey);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
const htmlPaths = inventory.pages.filter((page) => HTML_KINDS.has(page.kind)).map((page) => page.path).sort();
|
|
1382
|
+
const rankIndex = new Map(htmlPaths.map((path, index) => [path, index]));
|
|
1383
|
+
const rankTargets = htmlPaths.map((path) => (outbound.get(path) ?? []).map((edge) => rankIndex.get(edge.targetNodeKey)).filter((index) => index !== void 0));
|
|
1384
|
+
let ranks = htmlPaths.map(() => 1 / htmlPaths.length);
|
|
1385
|
+
for (let iteration = 0; iteration < 50; iteration++) {
|
|
1386
|
+
const next = htmlPaths.map(() => 0.15 / htmlPaths.length);
|
|
1387
|
+
let dangling = 0;
|
|
1388
|
+
for (const [index, targets] of rankTargets.entries()) {
|
|
1389
|
+
if (targets.length === 0) dangling += ranks[index];
|
|
1390
|
+
for (const target of targets) next[target] += 0.85 * ranks[index] / targets.length;
|
|
1391
|
+
}
|
|
1392
|
+
ranks = next.map((value) => value + 0.85 * dangling / htmlPaths.length);
|
|
1393
|
+
}
|
|
1394
|
+
const maxInbound = Math.max(1, ...[...inbound.values()].map((edges2) => edges2.length));
|
|
1395
|
+
const firstInbound = (path) => inbound.get(path)?.[0]?.sourceUrl ?? null;
|
|
1396
|
+
const pages = inventory.pages.map((page) => {
|
|
1397
|
+
const fetchState = page.kind === "broken" ? SiteCrawlFetchStates.fetchError : page.kind === "redirect" ? SiteCrawlFetchStates.redirect : page.kind === "resource" ? SiteCrawlFetchStates.nonHtml : SiteCrawlFetchStates.html;
|
|
1398
|
+
const indexabilityState = page.kind === "html" ? SiteCrawlIndexabilityStates.indexable : page.kind === "noindex" ? SiteCrawlIndexabilityStates.noindex : SiteCrawlIndexabilityStates.unknown;
|
|
1399
|
+
const indexabilityReasons = page.kind === "html" ? [] : page.kind === "noindex" ? [SiteCrawlIndexabilityReasons.metaRobotsNoindex] : page.kind === "canonicalized" ? [SiteCrawlIndexabilityReasons.canonicalToOther] : page.kind === "redirect" ? [SiteCrawlIndexabilityReasons.redirectTerminal] : [SiteCrawlIndexabilityReasons.notHtmlOrUnavailable];
|
|
1400
|
+
const canonicalNodeKey = page.kind === "canonicalized" ? page.canonicalTo : HTML_KINDS.has(page.kind) ? page.path : null;
|
|
1401
|
+
const audit = "score" in page ? auditFactors(page) : null;
|
|
1402
|
+
const inboundEdges = inbound.get(page.path) ?? [];
|
|
1403
|
+
const outboundEdges = outbound.get(page.path) ?? [];
|
|
1404
|
+
const rank = rankIndex.has(page.path) ? ranks[rankIndex.get(page.path)] : 0;
|
|
1405
|
+
const sitemapSources = inSitemap(page) ? [sitemapUrl] : [];
|
|
1406
|
+
return {
|
|
1407
|
+
...scope,
|
|
1408
|
+
id: `${prefix}-page-${page.path}`,
|
|
1409
|
+
nodeKey: page.path,
|
|
1410
|
+
url: url(page.path),
|
|
1411
|
+
path: page.path,
|
|
1412
|
+
parentPath: parentPath(page.path),
|
|
1413
|
+
discoverySource: page.path === ROOT_PATH ? "root" : sitemapSources.length > 0 ? "sitemap" : "link",
|
|
1414
|
+
discoveryProvenance: [{ discoveredFrom: firstInbound(page.path), sitemapSources, root: page.path === ROOT_PATH }],
|
|
1415
|
+
sitemapMetadata: { sources: sitemapSources },
|
|
1416
|
+
fetchState,
|
|
1417
|
+
fetchedAt: nowIso,
|
|
1418
|
+
httpStatus: page.kind === "broken" ? BROKEN_STATUS : page.kind === "redirect" ? 301 : 200,
|
|
1419
|
+
contentType: page.kind === "resource" ? page.contentType : page.kind === "redirect" ? null : "text/html; charset=utf-8",
|
|
1420
|
+
finalUrl: url(page.kind === "redirect" ? page.redirectTo : page.path),
|
|
1421
|
+
redirectChain: page.kind === "redirect" ? [url(page.redirectTo)] : [],
|
|
1422
|
+
directives: { metaRobots: page.kind === "noindex" ? ["follow", "noindex"] : [], xRobots: [] },
|
|
1423
|
+
canonicalUrl: canonicalNodeKey ? url(canonicalNodeKey) : null,
|
|
1424
|
+
canonicalNodeKey,
|
|
1425
|
+
indexabilityState,
|
|
1426
|
+
indexabilityReasons,
|
|
1427
|
+
healthState: deriveSiteHealthState({ nodeKey: page.path, fetchState, indexabilityState, indexabilityReasons, canonicalNodeKey }),
|
|
1428
|
+
auditState: audit ? "success" : page.kind === "broken" ? "error" : "not-applicable",
|
|
1429
|
+
auditScore: audit?.score ?? null,
|
|
1430
|
+
auditFields: audit ? { schemaVersion: "1.0", factors: audit.factors, criticalDefects: [] } : {},
|
|
1431
|
+
inventoryEligible: page.kind === "html",
|
|
1432
|
+
depth: depths.get(page.path) ?? null,
|
|
1433
|
+
inboundUniqueEdges: inboundEdges.length,
|
|
1434
|
+
outboundUniqueEdges: outboundEdges.length,
|
|
1435
|
+
inboundOccurrences: inboundEdges.reduce((sum, edge) => sum + edge.occurrences, 0),
|
|
1436
|
+
outboundOccurrences: outboundEdges.reduce((sum, edge) => sum + edge.occurrences, 0),
|
|
1437
|
+
// Raw score is PageRank, as the crawler computes it. Node size reads the normalized value on a 0 to 1
|
|
1438
|
+
// log scale of inbound links, so hub pages stay visibly larger than the pages they link to.
|
|
1439
|
+
linkScoreRaw: Number(rank.toFixed(12)),
|
|
1440
|
+
linkScoreNormalized: Math.log1p(inboundEdges.length) / Math.log1p(maxInbound),
|
|
1441
|
+
createdAt: nowIso,
|
|
1442
|
+
updatedAt: nowIso
|
|
1443
|
+
};
|
|
1444
|
+
});
|
|
1445
|
+
const findings = edges.flatMap((edge) => pagesByPath.get(edge.targetNodeKey)?.kind === "broken" ? [{
|
|
1446
|
+
...scope,
|
|
1447
|
+
id: `${prefix}-finding-${edge.edgeKey}`,
|
|
1448
|
+
findingKey: `dead-link:${edge.edgeKey}`,
|
|
1449
|
+
findingType: "dead-link",
|
|
1450
|
+
severity: "error",
|
|
1451
|
+
sourceNodeKey: edge.sourceNodeKey,
|
|
1452
|
+
sourceUrl: edge.sourceUrl,
|
|
1453
|
+
targetNodeKey: edge.targetNodeKey,
|
|
1454
|
+
targetUrl: edge.targetUrl,
|
|
1455
|
+
evidence: { statusCode: BROKEN_STATUS, reason: "http-error" },
|
|
1456
|
+
createdAt: nowIso,
|
|
1457
|
+
updatedAt: nowIso
|
|
1458
|
+
}] : []);
|
|
1459
|
+
const counts = {
|
|
1460
|
+
pagesDiscovered: pages.length,
|
|
1461
|
+
pagesFetched: pages.length,
|
|
1462
|
+
pagesEligible: pages.filter((page) => page.inventoryEligible).length,
|
|
1463
|
+
pagesErrored: pages.filter((page) => page.fetchState === SiteCrawlFetchStates.fetchError).length,
|
|
1464
|
+
edgesDiscovered: edges.length
|
|
1465
|
+
};
|
|
1466
|
+
const attempt = {
|
|
1467
|
+
...scope,
|
|
1468
|
+
id: input.attemptId,
|
|
1469
|
+
attemptNumber: 1,
|
|
1470
|
+
state: "completed",
|
|
1471
|
+
lastEventSequence: pages.length + edges.length + findings.length + 1,
|
|
1472
|
+
...counts,
|
|
1473
|
+
startedAt: nowIso,
|
|
1474
|
+
finishedAt: nowIso,
|
|
1475
|
+
createdAt: nowIso,
|
|
1476
|
+
updatedAt: nowIso
|
|
1477
|
+
};
|
|
1478
|
+
const pageBudget = Math.max(1e3, pages.length);
|
|
1479
|
+
const edgeBudget = Math.max(5e3, edges.length);
|
|
1480
|
+
const snapshot = {
|
|
1481
|
+
...scope,
|
|
1482
|
+
id: `${prefix}-crawl-snapshot`,
|
|
1483
|
+
rootUrl: root,
|
|
1484
|
+
requestedRootUrl: root,
|
|
1485
|
+
crawlSchemaVersion: "demo-2",
|
|
1486
|
+
engineVersion: "sample-seed",
|
|
1487
|
+
normalizationVersion: "demo-1",
|
|
1488
|
+
indexabilityVersion: "demo-1",
|
|
1489
|
+
linkScoreVersion: "demo-1",
|
|
1490
|
+
effectiveOptions: { mode: "summary", sitemapUrl, maxPages: pageBudget, maxEdges: edgeBudget, maxDepth: null, checkDeadLinks: true, sampleData: true },
|
|
1491
|
+
pageBudget,
|
|
1492
|
+
edgeBudget,
|
|
1493
|
+
maxDepth: null,
|
|
1494
|
+
checkDeadLinks: true,
|
|
1495
|
+
complete: true,
|
|
1496
|
+
termination: "complete",
|
|
1497
|
+
detailsAvailable: true,
|
|
1498
|
+
...counts,
|
|
1499
|
+
findingsCount: findings.length,
|
|
1500
|
+
deadLinkState: "complete",
|
|
1501
|
+
deadLinksChecked: new Set(edges.map((edge) => edge.targetNodeKey)).size,
|
|
1502
|
+
deadLinksFound: findings.length,
|
|
1503
|
+
deadLinksUnverified: 0,
|
|
1504
|
+
templateDetection: "applied-placement",
|
|
1505
|
+
linkPlacementRulesetVersion: "demo-1",
|
|
1506
|
+
createdAt: nowIso,
|
|
1507
|
+
updatedAt: nowIso
|
|
1508
|
+
};
|
|
1509
|
+
const order = [...pages].sort((left, right) => Number(right.nodeKey === ROOT_PATH) - Number(left.nodeKey === ROOT_PATH) || right.linkScoreNormalized - left.linkScoreNormalized || (left.nodeKey < right.nodeKey ? -1 : 1));
|
|
1510
|
+
const layoutEdges = [...edges].sort((left, right) => right.occurrences - left.occurrences || (left.edgeKey < right.edgeKey ? -1 : 1));
|
|
1511
|
+
const layoutInput = {
|
|
1512
|
+
rootNodeKey: ROOT_PATH,
|
|
1513
|
+
totalNodes: pages.length,
|
|
1514
|
+
totalEdges: edges.length,
|
|
1515
|
+
totalTemplateEdges: edges.filter((edge) => edge.isTemplate).length,
|
|
1516
|
+
nodes: order.map((page, sampleRank) => ({ nodeKey: page.nodeKey, path: page.path, depth: page.depth ?? null, sampleRank })),
|
|
1517
|
+
edges: layoutEdges.map((edge) => ({
|
|
1518
|
+
edgeKey: edge.edgeKey,
|
|
1519
|
+
sourceNodeKey: edge.sourceNodeKey,
|
|
1520
|
+
targetNodeKey: edge.targetNodeKey,
|
|
1521
|
+
followable: true,
|
|
1522
|
+
occurrences: edge.occurrences,
|
|
1523
|
+
isTemplate: edge.isTemplate
|
|
1524
|
+
}))
|
|
1525
|
+
};
|
|
1526
|
+
return { pages, edges, findings, attempt, snapshot, layoutInput };
|
|
1527
|
+
}
|
|
1528
|
+
async function seedSiteCrawl(db, input) {
|
|
1529
|
+
const crawl = buildDemoSiteCrawl(input);
|
|
1530
|
+
db.transaction((tx) => {
|
|
1531
|
+
tx.insert(siteCrawlAttempts).values(crawl.attempt).run();
|
|
1532
|
+
tx.insert(siteCrawlSnapshots).values(crawl.snapshot).run();
|
|
1533
|
+
for (let i = 0; i < crawl.pages.length; i += 100) tx.insert(siteCrawlPages).values(crawl.pages.slice(i, i + 100)).run();
|
|
1534
|
+
for (let i = 0; i < crawl.edges.length; i += 250) tx.insert(siteCrawlEdges).values(crawl.edges.slice(i, i + 250)).run();
|
|
1535
|
+
if (crawl.findings.length > 0) tx.insert(siteCrawlFindings).values(crawl.findings).run();
|
|
1536
|
+
});
|
|
1537
|
+
const layout = await layoutSiteCrawlGraphInput(crawl.layoutInput);
|
|
1538
|
+
if (layout.state !== "ready") throw new Error(`Demo site map for ${input.project.name} is unavailable: ${layout.failureCode}`);
|
|
1539
|
+
persistSiteCrawlGraphLayout(db, { projectId: input.project.id, runId: input.crawlRunId, attemptId: input.attemptId }, layout, input.nowIso);
|
|
1540
|
+
return { ...crawl, layout };
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// src/demo/site-inventories/harbor-resorts.ts
|
|
1544
|
+
var SECTIONS = [
|
|
1545
|
+
["rooms", ["ocean-suite", "family-suite", "garden-studio", "accessible-suite", "penthouse", "two-bedroom-villa", "king-room", "connecting-rooms"]],
|
|
1546
|
+
["dining", ["waterfront-grill", "poolside-cafe", "lobby-bar", "breakfast", "private-dining"]],
|
|
1547
|
+
["amenities", ["pool", "spa", "fitness-center", "beach", "kids-club", "pet-friendly-stays"]],
|
|
1548
|
+
["experiences", ["kayaking", "sunset-cruise", "local-food-tour", "nature-trails", "art-walk", "family-activities"]],
|
|
1549
|
+
["offers", ["family-getaway", "weekend-escape", "extended-stay", "spa-retreat"]],
|
|
1550
|
+
["meetings", ["ballroom", "boardroom", "waterfront-terrace"]]
|
|
1551
|
+
];
|
|
1552
|
+
var GUIDE_TOPICS = ["getting-here", "family-travel", "seasonal-events", "local-restaurants", "weekend-itinerary"];
|
|
1553
|
+
function parentOf(path) {
|
|
1554
|
+
const parts = path.split("/").filter(Boolean);
|
|
1555
|
+
return parts.length > 1 ? `/${parts.slice(0, -1).join("/")}/` : "/";
|
|
1556
|
+
}
|
|
1557
|
+
function harborResortsInventory() {
|
|
1558
|
+
const pages = [];
|
|
1559
|
+
const links = [];
|
|
1560
|
+
const html = (path, score = 92) => pages.push({ path, kind: "html", score });
|
|
1561
|
+
const content = (source, target) => links.push([source, target, "content"]);
|
|
1562
|
+
for (const path of ["/", "/destinations/", "/offers/", "/experiences/", "/guides/", "/contact/", "/services/"]) html(path);
|
|
1563
|
+
for (const market of HARBOR_MARKETS) {
|
|
1564
|
+
html(`/destinations/${market.key}/`);
|
|
1565
|
+
html(`/destinations/${market.key}/guides/`);
|
|
1566
|
+
for (const topic of GUIDE_TOPICS) html(`/destinations/${market.key}/guides/${topic}/`, 82);
|
|
1567
|
+
content("/guides/", `/destinations/${market.key}/guides/`);
|
|
1568
|
+
}
|
|
1569
|
+
const properties = harborProperties();
|
|
1570
|
+
for (const [propertyIndex, property] of properties.entries()) {
|
|
1571
|
+
const home = `${property.path}/`;
|
|
1572
|
+
html(home, 94);
|
|
1573
|
+
for (const [sectionIndex, [section, topics]] of SECTIONS.entries()) {
|
|
1574
|
+
html(`${home}${section}/`, 88 - propertyIndex % 5);
|
|
1575
|
+
for (const [topicIndex, topic] of topics.entries()) {
|
|
1576
|
+
const pagePath = `${home}${section}/${topic}/`;
|
|
1577
|
+
html(pagePath, 94 - (propertyIndex * 7 + sectionIndex * 3 + topicIndex * 9) % 39);
|
|
1578
|
+
content(pagePath, `${home}${section}/${topics[(topicIndex + 1) % topics.length]}/`);
|
|
1579
|
+
content(pagePath, `${home}${section}/${topics[(topicIndex + 2) % topics.length]}/`);
|
|
1580
|
+
content(pagePath, `${home}offers/family-getaway/`);
|
|
1581
|
+
content(pagePath, `${home}amenities/spa/`);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
for (const section of ["gallery", "location", "faq", "reviews", "contact"]) html(`${home}${section}/`, 86);
|
|
1585
|
+
pages.push(
|
|
1586
|
+
{ path: `${home}booking-confirmation/`, kind: "noindex", score: 90 },
|
|
1587
|
+
{ path: `${home}offers/expired-summer-package/`, kind: "broken" },
|
|
1588
|
+
{ path: `${home}property-guide.pdf`, kind: "resource", contentType: "application/pdf" },
|
|
1589
|
+
{ path: `${home}rooms/old-ocean-suite/`, kind: "redirect", redirectTo: `${home}rooms/ocean-suite/` }
|
|
1590
|
+
);
|
|
1591
|
+
content("/offers/", `${home}offers/`);
|
|
1592
|
+
const siblings = properties.filter((candidate) => candidate.market.key === property.market.key);
|
|
1593
|
+
content(home, `${siblings[property.number % siblings.length].path}/`);
|
|
1594
|
+
content(`/destinations/${property.market.key}/guides/family-travel/`, home);
|
|
1595
|
+
}
|
|
1596
|
+
for (const page of pages) {
|
|
1597
|
+
const parent = parentOf(page.path);
|
|
1598
|
+
if (page.path !== "/") content(parent, page.path);
|
|
1599
|
+
if (page.kind === "broken" || page.kind === "resource" || page.kind === "redirect") continue;
|
|
1600
|
+
links.push([page.path, "/", "navigation"], [page.path, parent, "navigation"]);
|
|
1601
|
+
const property = properties.find((candidate) => page.path.startsWith(`${candidate.path}/`));
|
|
1602
|
+
if (property) links.push([page.path, `${property.path}/`, "navigation"]);
|
|
1603
|
+
}
|
|
1604
|
+
return { pages, templateLinks: [], links };
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// src/demo/site-inventories/summit-roofing.ts
|
|
1608
|
+
var SERVICES = [
|
|
1609
|
+
["roof-replacement", ["asphalt-shingles", "metal-roofing", "tile-roofing", "flat-roofing", "cedar-shake"]],
|
|
1610
|
+
["roof-repair", ["leak-repair", "storm-damage", "emergency-tarping", "flashing-repair", "shingle-replacement"]],
|
|
1611
|
+
["roof-inspection", ["drone-inspection", "insurance-claims", "real-estate-inspection"]],
|
|
1612
|
+
["gutters", ["seamless-gutters", "gutter-guards", "downspouts"]],
|
|
1613
|
+
["skylights", ["skylight-installation", "skylight-repair"]],
|
|
1614
|
+
["commercial-roofing", ["tpo-roofing", "epdm-roofing", "roof-coatings"]],
|
|
1615
|
+
["siding", ["vinyl-siding", "fiber-cement-siding"]]
|
|
1616
|
+
];
|
|
1617
|
+
var TOWNS = [
|
|
1618
|
+
"alder-creek",
|
|
1619
|
+
"birch-hollow",
|
|
1620
|
+
"cedar-bluff",
|
|
1621
|
+
"copper-ridge",
|
|
1622
|
+
"elm-crossing",
|
|
1623
|
+
"fox-meadow",
|
|
1624
|
+
"hawthorn-glen",
|
|
1625
|
+
"ironwood-flats",
|
|
1626
|
+
"juniper-springs",
|
|
1627
|
+
"kestrel-point",
|
|
1628
|
+
"larkspur-hills",
|
|
1629
|
+
"millbrook",
|
|
1630
|
+
"north-quarry",
|
|
1631
|
+
"oak-terrace",
|
|
1632
|
+
"pine-hollow",
|
|
1633
|
+
"quail-run",
|
|
1634
|
+
"redfern",
|
|
1635
|
+
"stonegate",
|
|
1636
|
+
"thistle-ridge",
|
|
1637
|
+
"upper-brookside",
|
|
1638
|
+
"vale-junction",
|
|
1639
|
+
"willow-bend",
|
|
1640
|
+
"yarrow-heights",
|
|
1641
|
+
"zephyr-lake"
|
|
1642
|
+
];
|
|
1643
|
+
var TOWN_SERVICES = ["roof-replacement", "roof-repair", "gutters"];
|
|
1644
|
+
var PROJECT_TYPES = [
|
|
1645
|
+
["metal-roof-replacement", "roof-replacement", "metal-roofing"],
|
|
1646
|
+
["asphalt-shingle-roof", "roof-replacement", "asphalt-shingles"],
|
|
1647
|
+
["storm-damage-repair", "roof-repair", "storm-damage"],
|
|
1648
|
+
["seamless-gutter-install", "gutters", "seamless-gutters"],
|
|
1649
|
+
["skylight-replacement", "skylights", "skylight-installation"],
|
|
1650
|
+
["tpo-commercial-roof", "commercial-roofing", "tpo-roofing"],
|
|
1651
|
+
["cedar-shake-restoration", "roof-replacement", "cedar-shake"],
|
|
1652
|
+
["fiber-cement-siding", "siding", "fiber-cement-siding"],
|
|
1653
|
+
["emergency-leak-repair", "roof-repair", "leak-repair"]
|
|
1654
|
+
];
|
|
1655
|
+
var GUIDE_TOPICS2 = ["materials", "repair", "insurance", "maintenance", "costs", "commercial"];
|
|
1656
|
+
var GUIDES = [
|
|
1657
|
+
["how-much-does-a-roof-replacement-cost", "roof-replacement", "costs"],
|
|
1658
|
+
["signs-you-need-a-new-roof", "roof-replacement", "maintenance"],
|
|
1659
|
+
["metal-vs-asphalt-shingles", "roof-replacement", "materials"],
|
|
1660
|
+
["how-long-does-a-roof-last", "roof-replacement", "materials"],
|
|
1661
|
+
["roof-repair-vs-replacement", "roof-repair", "costs"],
|
|
1662
|
+
["what-to-do-after-hail-damage", "roof-repair", "insurance"],
|
|
1663
|
+
["filing-a-roof-insurance-claim", "roof-inspection", "insurance"],
|
|
1664
|
+
["choosing-a-roofing-contractor", "roof-replacement", "costs"],
|
|
1665
|
+
["ice-dam-prevention", "roof-repair", "maintenance"],
|
|
1666
|
+
["how-to-spot-a-roof-leak", "roof-repair", "repair"],
|
|
1667
|
+
["attic-ventilation-basics", "roof-replacement", "maintenance"],
|
|
1668
|
+
["flat-roof-materials-compared", "commercial-roofing", "materials"],
|
|
1669
|
+
["tpo-vs-epdm", "commercial-roofing", "commercial"],
|
|
1670
|
+
["gutter-guard-types", "gutters", "maintenance"],
|
|
1671
|
+
["when-to-replace-gutters", "gutters", "repair"],
|
|
1672
|
+
["skylight-leak-causes", "skylights", "repair"],
|
|
1673
|
+
["cedar-shake-maintenance", "roof-replacement", "maintenance"],
|
|
1674
|
+
["tile-roof-lifespan", "roof-replacement", "materials"],
|
|
1675
|
+
["roof-inspection-checklist", "roof-inspection", "maintenance"],
|
|
1676
|
+
["what-a-roof-warranty-covers", "roof-replacement", "costs"],
|
|
1677
|
+
["financing-a-new-roof", "roof-replacement", "costs"],
|
|
1678
|
+
["storm-season-preparation", "roof-repair", "insurance"],
|
|
1679
|
+
["emergency-tarping-what-to-expect", "roof-repair", "repair"],
|
|
1680
|
+
["roof-flashing-explained", "roof-repair", "repair"],
|
|
1681
|
+
["shingle-color-and-energy-costs", "roof-replacement", "materials"],
|
|
1682
|
+
["roof-coatings-for-commercial-buildings", "commercial-roofing", "commercial"],
|
|
1683
|
+
["vinyl-vs-fiber-cement-siding", "siding", "materials"],
|
|
1684
|
+
["how-drone-roof-inspections-work", "roof-inspection", "insurance"],
|
|
1685
|
+
["preparing-for-roof-replacement-day", "roof-replacement", "costs"],
|
|
1686
|
+
["roof-replacement-timeline", "roof-replacement", "costs"],
|
|
1687
|
+
["moss-and-algae-removal", "roof-repair", "maintenance"],
|
|
1688
|
+
["chimney-flashing-repair", "roof-repair", "repair"],
|
|
1689
|
+
["roof-underlayment-types", "roof-replacement", "materials"],
|
|
1690
|
+
["solar-ready-roofing", "roof-replacement", "materials"],
|
|
1691
|
+
["commercial-roof-maintenance-plans", "commercial-roofing", "commercial"],
|
|
1692
|
+
["downspout-drainage-tips", "gutters", "maintenance"],
|
|
1693
|
+
["real-estate-roof-inspections", "roof-inspection", "insurance"],
|
|
1694
|
+
["winter-roof-care", "roof-repair", "maintenance"],
|
|
1695
|
+
["wind-damage-signs", "roof-repair", "insurance"],
|
|
1696
|
+
["roof-pitch-explained", "roof-replacement", "materials"],
|
|
1697
|
+
["recycling-old-shingles", "roof-replacement", "materials"],
|
|
1698
|
+
["metal-roof-noise", "roof-replacement", "materials"],
|
|
1699
|
+
["what-to-ask-before-signing", "roof-replacement", "costs"],
|
|
1700
|
+
["skylight-sizing-guide", "skylights", "materials"]
|
|
1701
|
+
];
|
|
1702
|
+
var HEADER = ["/", "/services/", "/service-areas/", "/projects/", "/reviews/", "/financing/", "/about/", "/contact/", "/get-a-quote/"];
|
|
1703
|
+
var FOOTER = [
|
|
1704
|
+
"/services/roof-replacement/",
|
|
1705
|
+
"/services/roof-repair/",
|
|
1706
|
+
"/services/gutters/",
|
|
1707
|
+
"/services/commercial-roofing/",
|
|
1708
|
+
"/guides/",
|
|
1709
|
+
"/about/warranty/",
|
|
1710
|
+
"/careers/",
|
|
1711
|
+
"/contact/",
|
|
1712
|
+
"/privacy-policy/",
|
|
1713
|
+
"/terms/",
|
|
1714
|
+
"/accessibility/"
|
|
1715
|
+
];
|
|
1716
|
+
var ORPHAN_TOWN_SERVICE = `/service-areas/${TOWNS[5]}/gutters/`;
|
|
1717
|
+
var THIN_TOWN_SERVICES = /* @__PURE__ */ new Set([
|
|
1718
|
+
`/service-areas/${TOWNS[2]}/roof-repair/`,
|
|
1719
|
+
`/service-areas/${TOWNS[3]}/gutters/`,
|
|
1720
|
+
`/service-areas/${TOWNS[4]}/roof-repair/`,
|
|
1721
|
+
`/service-areas/${TOWNS[5]}/roof-replacement/`
|
|
1722
|
+
]);
|
|
1723
|
+
var THIN_PROJECTS = /* @__PURE__ */ new Set([8, 19, 30]);
|
|
1724
|
+
var service = (key) => `/services/${key}/`;
|
|
1725
|
+
var town = (key) => `/service-areas/${key}/`;
|
|
1726
|
+
var guide = (slug) => `/guides/${slug}/`;
|
|
1727
|
+
function summitRoofingInventory() {
|
|
1728
|
+
const pages = [];
|
|
1729
|
+
const links = [];
|
|
1730
|
+
const link = (source, ...targets) => {
|
|
1731
|
+
for (const target of targets) links.push([source, target]);
|
|
1732
|
+
};
|
|
1733
|
+
const projects2 = Array.from({ length: 36 }, (_, index) => {
|
|
1734
|
+
const [type, category, sub] = PROJECT_TYPES[index % PROJECT_TYPES.length];
|
|
1735
|
+
const townKey = TOWNS[index % TOWNS.length];
|
|
1736
|
+
return { index, path: `/projects/${townKey}-${type}/`, townKey, category, sub };
|
|
1737
|
+
});
|
|
1738
|
+
pages.push(
|
|
1739
|
+
{ path: "/", kind: "html", score: 88 },
|
|
1740
|
+
{ path: "/llms.txt", kind: "resource", contentType: "text/plain; charset=utf-8", sitemap: true },
|
|
1741
|
+
{ path: "/services/", kind: "html", score: 84 }
|
|
1742
|
+
);
|
|
1743
|
+
link(
|
|
1744
|
+
"/",
|
|
1745
|
+
...SERVICES.map(([key]) => service(key)),
|
|
1746
|
+
"/service-areas/",
|
|
1747
|
+
...projects2.slice(0, 3).map((project) => project.path),
|
|
1748
|
+
guide(GUIDES[0][0]),
|
|
1749
|
+
guide(GUIDES[2][0]),
|
|
1750
|
+
guide(GUIDES[5][0]),
|
|
1751
|
+
"/reviews/",
|
|
1752
|
+
"/financing/",
|
|
1753
|
+
"/get-a-quote/"
|
|
1754
|
+
);
|
|
1755
|
+
link("/services/", ...SERVICES.map(([key]) => service(key)), "/service-areas/");
|
|
1756
|
+
for (const [categoryIndex, [category, subs]] of SERVICES.entries()) {
|
|
1757
|
+
pages.push({ path: service(category), kind: "html", score: 86 - categoryIndex * 2 });
|
|
1758
|
+
const categoryGuides = GUIDES.filter(([, key]) => key === category);
|
|
1759
|
+
const categoryProjects = projects2.filter((project) => project.category === category);
|
|
1760
|
+
link(
|
|
1761
|
+
service(category),
|
|
1762
|
+
...subs.map((sub) => service(`${category}/${sub}`)),
|
|
1763
|
+
"/get-a-quote/",
|
|
1764
|
+
"/financing/",
|
|
1765
|
+
...categoryGuides.slice(0, 2).map(([slug]) => guide(slug)),
|
|
1766
|
+
...categoryProjects.slice(0, 2).map((project) => project.path)
|
|
1767
|
+
);
|
|
1768
|
+
for (const [subIndex, sub] of subs.entries()) {
|
|
1769
|
+
const path = service(`${category}/${sub}`);
|
|
1770
|
+
pages.push({ path, kind: "html", score: 83 - (subIndex * 7 + categoryIndex * 3) % 17 });
|
|
1771
|
+
const project = categoryProjects.find((candidate) => candidate.sub === sub) ?? categoryProjects.at(0);
|
|
1772
|
+
link(path, service(category), service(`${category}/${subs[(subIndex + 1) % subs.length]}`), "/get-a-quote/");
|
|
1773
|
+
if (project) link(path, project.path);
|
|
1774
|
+
const related = categoryGuides.at(subIndex % Math.max(1, categoryGuides.length));
|
|
1775
|
+
if (related) link(path, guide(related[0]));
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
pages.push({ path: "/service-areas/", kind: "html", score: 78 });
|
|
1779
|
+
link("/service-areas/", ...TOWNS.map(town));
|
|
1780
|
+
for (const [townIndex, townKey] of TOWNS.entries()) {
|
|
1781
|
+
pages.push({ path: town(townKey), kind: "html", score: 76 - townIndex % 6 * 2, factorScores: { "geographic-signals": 92 } });
|
|
1782
|
+
link(
|
|
1783
|
+
town(townKey),
|
|
1784
|
+
service("roof-replacement"),
|
|
1785
|
+
service("roof-repair"),
|
|
1786
|
+
service("gutters"),
|
|
1787
|
+
service("roof-inspection"),
|
|
1788
|
+
town(TOWNS[(townIndex + 1) % TOWNS.length]),
|
|
1789
|
+
...projects2.filter((project) => project.townKey === townKey).map((project) => project.path),
|
|
1790
|
+
"/reviews/",
|
|
1791
|
+
"/get-a-quote/"
|
|
1792
|
+
);
|
|
1793
|
+
if (townIndex >= 6) continue;
|
|
1794
|
+
for (const serviceKey of TOWN_SERVICES) {
|
|
1795
|
+
const path = `${town(townKey)}${serviceKey}/`;
|
|
1796
|
+
const thin = THIN_TOWN_SERVICES.has(path);
|
|
1797
|
+
pages.push({ path, kind: "html", score: thin ? 58 : 72, factorScores: thin ? { "content-depth": 34, "geographic-signals": 88 } : { "geographic-signals": 94 } });
|
|
1798
|
+
link(path, town(townKey), service(serviceKey), "/get-a-quote/");
|
|
1799
|
+
if (path !== ORPHAN_TOWN_SERVICE) link(town(townKey), path);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
pages.push({ path: "/projects/", kind: "html", score: 80 });
|
|
1803
|
+
link("/projects/", ...projects2.map((project) => project.path));
|
|
1804
|
+
for (const project of projects2) {
|
|
1805
|
+
const thin = THIN_PROJECTS.has(project.index);
|
|
1806
|
+
pages.push({ path: project.path, kind: "html", score: thin ? 60 : 77 - project.index % 7 * 2, factorScores: thin ? { "content-depth": 31 } : void 0 });
|
|
1807
|
+
link(
|
|
1808
|
+
project.path,
|
|
1809
|
+
service(project.category),
|
|
1810
|
+
service(`${project.category}/${project.sub}`),
|
|
1811
|
+
town(project.townKey),
|
|
1812
|
+
projects2[(project.index + 1) % projects2.length].path,
|
|
1813
|
+
"/get-a-quote/",
|
|
1814
|
+
"/reviews/"
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
pages.push(
|
|
1818
|
+
{ path: "/guides/", kind: "html", score: 83 },
|
|
1819
|
+
{ path: "/guides/page/2/", kind: "canonicalized", canonicalTo: "/guides/", score: 72 },
|
|
1820
|
+
{ path: "/guides/page/3/", kind: "canonicalized", canonicalTo: "/guides/", score: 70 },
|
|
1821
|
+
{ path: "/guides/topic/", kind: "noindex", score: 70 },
|
|
1822
|
+
{ path: "/guides/roof-maintenance-checklist.pdf", kind: "resource", contentType: "application/pdf" }
|
|
1823
|
+
);
|
|
1824
|
+
link("/guides/", ...GUIDES.slice(0, 20).map(([slug]) => guide(slug)), "/guides/page/2/", "/guides/topic/");
|
|
1825
|
+
link("/guides/page/2/", ...GUIDES.slice(20, 40).map(([slug]) => guide(slug)), "/guides/", "/guides/page/3/");
|
|
1826
|
+
link("/guides/page/3/", ...GUIDES.slice(40).map(([slug]) => guide(slug)), "/guides/page/2/");
|
|
1827
|
+
link("/guides/topic/", ...GUIDE_TOPICS2.map((topic) => `/guides/topic/${topic}/`));
|
|
1828
|
+
for (const topic of GUIDE_TOPICS2) {
|
|
1829
|
+
pages.push({ path: `/guides/topic/${topic}/`, kind: "noindex", score: 68 });
|
|
1830
|
+
link(`/guides/topic/${topic}/`, "/guides/", ...GUIDES.filter(([, , guideTopic]) => guideTopic === topic).map(([slug]) => guide(slug)));
|
|
1831
|
+
}
|
|
1832
|
+
for (const [guideIndex, [slug, serviceKey, topic]] of GUIDES.entries()) {
|
|
1833
|
+
const noAuthor = guideIndex % 5 === 4;
|
|
1834
|
+
pages.push({ path: guide(slug), kind: "html", score: 81 - guideIndex % 6 * 3, factorScores: noAuthor ? { "eeat-signals": 28 } : void 0 });
|
|
1835
|
+
link(guide(slug), service(serviceKey), guide(GUIDES[(guideIndex + 1) % GUIDES.length][0]), `/guides/topic/${topic}/`, "/get-a-quote/");
|
|
1836
|
+
}
|
|
1837
|
+
link(guide(GUIDES[18][0]), "/guides/roof-maintenance-checklist.pdf");
|
|
1838
|
+
link(guide("what-a-roof-warranty-covers"), "/about/warranty/");
|
|
1839
|
+
pages.push(
|
|
1840
|
+
{ path: "/financing/", kind: "html", score: 79 },
|
|
1841
|
+
{ path: "/financing/apply/", kind: "html", score: 71 },
|
|
1842
|
+
{ path: "/financing/faq/", kind: "html", score: 86, factorScores: { "faq-content": 95 } },
|
|
1843
|
+
{ path: "/financing/financing-terms.pdf", kind: "resource", contentType: "application/pdf" },
|
|
1844
|
+
{ path: "/reviews/", kind: "html", score: 85 },
|
|
1845
|
+
{ path: "/reviews/leave-a-review/", kind: "noindex", score: 60 },
|
|
1846
|
+
{ path: "/about/", kind: "html", score: 82 },
|
|
1847
|
+
{ path: "/about/team/", kind: "html", score: 80, factorScores: { "eeat-signals": 93 } },
|
|
1848
|
+
{ path: "/about/certifications/", kind: "html", score: 83 },
|
|
1849
|
+
{ path: "/about/warranty/", kind: "html", score: 81 },
|
|
1850
|
+
{ path: "/about/community/", kind: "html", score: 72 },
|
|
1851
|
+
{ path: "/about/warranty/warranty-guide.pdf", kind: "resource", contentType: "application/pdf" },
|
|
1852
|
+
{ path: "/careers/", kind: "html", score: 70 },
|
|
1853
|
+
{ path: "/careers/roofing-crew-lead/", kind: "html", score: 66 },
|
|
1854
|
+
{ path: "/careers/project-manager/", kind: "html", score: 67 },
|
|
1855
|
+
{ path: "/careers/sales-estimator/", kind: "html", score: 65 },
|
|
1856
|
+
{ path: "/contact/", kind: "html", score: 84 },
|
|
1857
|
+
{ path: "/contact/thank-you/", kind: "noindex", score: 50 },
|
|
1858
|
+
{ path: "/get-a-quote/", kind: "html", score: 80 },
|
|
1859
|
+
{ path: "/get-a-quote/roof-details/", kind: "noindex", score: 62 },
|
|
1860
|
+
{ path: "/get-a-quote/schedule/", kind: "noindex", score: 62 },
|
|
1861
|
+
{ path: "/get-a-quote/thank-you/", kind: "noindex", score: 50 },
|
|
1862
|
+
{ path: "/privacy-policy/", kind: "html", score: 64 },
|
|
1863
|
+
{ path: "/terms/", kind: "html", score: 63 },
|
|
1864
|
+
{ path: "/accessibility/", kind: "html", score: 69 }
|
|
1865
|
+
);
|
|
1866
|
+
link("/financing/", "/financing/apply/", "/financing/faq/", "/financing/financing-terms.pdf", "/get-a-quote/");
|
|
1867
|
+
link("/financing/faq/", "/financing/apply/");
|
|
1868
|
+
link("/reviews/", "/reviews/leave-a-review/", ...projects2.slice(0, 6).map((project) => project.path));
|
|
1869
|
+
link("/about/", "/about/team/", "/about/certifications/", "/about/warranty/", "/about/community/", "/reviews/");
|
|
1870
|
+
link("/about/warranty/", "/about/warranty/warranty-guide.pdf", guide("what-a-roof-warranty-covers"));
|
|
1871
|
+
link("/about/certifications/", service("roof-replacement"));
|
|
1872
|
+
link("/about/community/", "/projects/");
|
|
1873
|
+
link("/careers/", "/careers/roofing-crew-lead/", "/careers/project-manager/", "/careers/sales-estimator/");
|
|
1874
|
+
for (const role of ["roofing-crew-lead", "project-manager", "sales-estimator"]) link(`/careers/${role}/`, "/careers/");
|
|
1875
|
+
link("/contact/", "/contact/thank-you/");
|
|
1876
|
+
link("/get-a-quote/", "/get-a-quote/roof-details/", "/financing/");
|
|
1877
|
+
link("/get-a-quote/roof-details/", "/get-a-quote/schedule/");
|
|
1878
|
+
link("/get-a-quote/schedule/", "/get-a-quote/thank-you/");
|
|
1879
|
+
pages.push(
|
|
1880
|
+
{ path: "/roof-repair/", kind: "redirect", redirectTo: service("roof-repair") },
|
|
1881
|
+
{ path: "/blog/", kind: "redirect", redirectTo: "/guides/" },
|
|
1882
|
+
{ path: "/services/metal-roofs/", kind: "redirect", redirectTo: service("roof-replacement/metal-roofing") },
|
|
1883
|
+
{ path: "/guides/ice-dam-prevention-2023/", kind: "redirect", redirectTo: guide("ice-dam-prevention") },
|
|
1884
|
+
{ path: "/service-areas/old-county/", kind: "redirect", redirectTo: "/service-areas/" },
|
|
1885
|
+
{ path: "/free-estimate/", kind: "redirect", redirectTo: "/get-a-quote/" }
|
|
1886
|
+
);
|
|
1887
|
+
link(service("roof-repair/leak-repair"), "/roof-repair/");
|
|
1888
|
+
link(guide("how-to-spot-a-roof-leak"), "/roof-repair/");
|
|
1889
|
+
link(guide("signs-you-need-a-new-roof"), "/blog/");
|
|
1890
|
+
link(guide("roof-repair-vs-replacement"), "/blog/");
|
|
1891
|
+
link(guide("metal-vs-asphalt-shingles"), "/services/metal-roofs/");
|
|
1892
|
+
link(guide("winter-roof-care"), "/guides/ice-dam-prevention-2023/");
|
|
1893
|
+
link(town(TOWNS.at(-1)), "/service-areas/old-county/");
|
|
1894
|
+
link(projects2[0].path, "/free-estimate/");
|
|
1895
|
+
link("/financing/faq/", "/free-estimate/");
|
|
1896
|
+
pages.push(
|
|
1897
|
+
{ path: "/careers/estimator-2025/", kind: "broken" },
|
|
1898
|
+
{ path: "/financing/spring-promo/", kind: "broken" },
|
|
1899
|
+
{ path: "/guides/hail-season-update/", kind: "broken" },
|
|
1900
|
+
{ path: `/projects/${TOWNS[2]}-reroof-draft/`, kind: "broken" }
|
|
1901
|
+
);
|
|
1902
|
+
link("/careers/", "/careers/estimator-2025/");
|
|
1903
|
+
link("/financing/", "/financing/spring-promo/");
|
|
1904
|
+
link(guide("what-to-do-after-hail-damage"), "/guides/hail-season-update/");
|
|
1905
|
+
link(guide("storm-season-preparation"), "/guides/hail-season-update/");
|
|
1906
|
+
link("/projects/", `/projects/${TOWNS[2]}-reroof-draft/`);
|
|
1907
|
+
pages.push(
|
|
1908
|
+
{ path: "/spring-roof-special/", kind: "html", score: 52, factorScores: { "content-freshness": 18 } },
|
|
1909
|
+
{ path: "/guides/choosing-roof-color/", kind: "html", score: 74 }
|
|
1910
|
+
);
|
|
1911
|
+
link("/spring-roof-special/", "/get-a-quote/");
|
|
1912
|
+
link("/guides/choosing-roof-color/", service("roof-replacement"));
|
|
1913
|
+
return { pages, templateLinks: [...HEADER, ...FOOTER], links };
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// src/demo/seed-signals.ts
|
|
1917
|
+
var day = (at2, offset) => {
|
|
1918
|
+
const value = new Date(at2);
|
|
1919
|
+
value.setUTCDate(value.getUTCDate() + offset);
|
|
1920
|
+
return value.toISOString().slice(0, 10);
|
|
1921
|
+
};
|
|
1922
|
+
var at = (date) => `${date}T12:00:00.000Z`;
|
|
1923
|
+
async function seedDemoSignals(db, context) {
|
|
1924
|
+
for (const [index, project] of [context.simple, context.portfolio].entries()) {
|
|
1925
|
+
await seedProjectSignals(db, project, context.now, index);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
async function seedProjectSignals(db, project, now, variant) {
|
|
1929
|
+
const nowIso = now.toISOString();
|
|
1930
|
+
const prefix = `demo-signals-${project.id}`;
|
|
1931
|
+
const syncRunId = `${prefix}-sync`;
|
|
1932
|
+
const crawlRunId = `${prefix}-crawl`;
|
|
1933
|
+
const root = `https://${project.domain}/`;
|
|
1934
|
+
const attemptId = `${prefix}-crawl-attempt`;
|
|
1935
|
+
const campaignId = `${prefix}-campaign`;
|
|
1936
|
+
const groupId = `${prefix}-ad-group`;
|
|
1937
|
+
const locationName = `locations/demo-${variant + 1}`;
|
|
1938
|
+
db.insert(runs).values([
|
|
1939
|
+
{ id: syncRunId, projectId: project.id, kind: "data-refresh", status: "completed", trigger: "manual", startedAt: nowIso, finishedAt: nowIso, createdAt: nowIso },
|
|
1940
|
+
{ id: crawlRunId, projectId: project.id, kind: "site-audit", status: "completed", trigger: "manual", startedAt: nowIso, finishedAt: nowIso, createdAt: nowIso }
|
|
1941
|
+
]).run();
|
|
1942
|
+
db.insert(googleConnections).values({ id: `${prefix}-gsc`, domain: project.domain, connectionType: "gsc", propertyId: `sc-domain:${project.domain}`, scopes: [], createdByProjectId: project.id, createdAt: nowIso, updatedAt: nowIso }).run();
|
|
1943
|
+
db.insert(bingConnections).values({ id: `${prefix}-bing`, domain: project.domain, siteUrl: root, createdByProjectId: project.id, createdAt: nowIso, updatedAt: nowIso }).run();
|
|
1944
|
+
const searchTerms = variant === 0 ? ["roof replacement costs", "emergency roof repair options"] : ["family resorts near the coast", "coastal resort amenities"];
|
|
1945
|
+
const dates = Array.from({ length: 14 }, (_, i) => day(now, i - 14));
|
|
1946
|
+
for (const [i, date] of dates.entries()) {
|
|
1947
|
+
const clicks = 34 + i * 3 + variant * 5;
|
|
1948
|
+
const impressions = 620 + i * 31 + variant * 55;
|
|
1949
|
+
db.insert(gscDailyTotals).values({ id: `${prefix}-gsc-total-${date}`, projectId: project.id, date, clicks, impressions, position: (8.8 - i * 0.08).toFixed(2), createdAt: at(date) }).run();
|
|
1950
|
+
for (const [termIndex, query] of searchTerms.entries()) {
|
|
1951
|
+
const queryClicks = Math.max(3, Math.round(clicks / (termIndex + 2)));
|
|
1952
|
+
const queryImpressions = Math.round(impressions / (termIndex + 2));
|
|
1953
|
+
db.insert(gscQueryDailyTotals).values({ id: `${prefix}-gsc-query-${i}-${termIndex}`, projectId: project.id, date, query, clicks: queryClicks, impressions: queryImpressions, position: (7.1 + termIndex + i * 0.03).toFixed(2), syncedAt: nowIso, syncRunId, createdAt: at(date) }).run();
|
|
1954
|
+
db.insert(gscSearchData).values({ id: `${prefix}-gsc-page-${i}-${termIndex}`, projectId: project.id, syncRunId, date, query, page: termIndex === 0 ? root : `${root}services/`, country: "usa", device: "DESKTOP", clicks: queryClicks, impressions: queryImpressions, ctr: (queryClicks / queryImpressions).toFixed(4), position: (7.1 + termIndex).toFixed(2), createdAt: at(date) }).run();
|
|
1955
|
+
}
|
|
1956
|
+
db.insert(gaDailyTotals).values({ id: `${prefix}-ga-total-${date}`, projectId: project.id, date, sessions: 92 + i * 4 + variant * 11, users: 71 + i * 3 + variant * 8, engagementRate: 0.58 + i * 4e-3, newUsers: 29 + i, syncedAt: nowIso, syncRunId, createdAt: at(date) }).run();
|
|
1957
|
+
db.insert(gaTrafficSnapshots).values({ id: `${prefix}-ga-page-${date}`, projectId: project.id, date, landingPage: "/", landingPageNormalized: "/", sessions: 62 + i * 3, organicSessions: 39 + i * 2, directSessions: 14 + i, users: 48 + i * 2, syncedAt: nowIso, syncRunId }).run();
|
|
1958
|
+
db.insert(gaAcquisitionDaily).values({ id: `${prefix}-ga-acquisition-${date}`, projectId: project.id, date, channelGroup: "Organic Search", source: "google", medium: "organic", hostName: project.domain, landingPage: "/", landingPageNormalized: "/", sessions: 39 + i * 2, syncedAt: nowIso, syncRunId, createdAt: at(date) }).run();
|
|
1959
|
+
db.insert(gaLeadEventsDaily).values({ id: `${prefix}-ga-lead-${date}`, projectId: project.id, date, eventName: "generate_lead", channelGroup: "Organic Search", source: "google", medium: "organic", hostName: project.domain, landingPage: "/", landingPageNormalized: "/", attributionScope: "landing-page", eventCount: 3 + i % 4, syncedAt: nowIso, syncRunId, createdAt: at(date) }).run();
|
|
1960
|
+
db.insert(gaAiReferrals).values({ id: `${prefix}-ga-ai-${date}`, projectId: project.id, date, source: i % 2 ? "demo-answer-engine.example" : "demo-ai-referral.example", medium: "referral", trafficClass: "organic", sourceDimension: "session", channelGroup: "Referral", landingPage: "/", landingPageNormalized: "/", sessions: 4 + i % 3, users: 3 + i % 2, syncedAt: nowIso, syncRunId }).run();
|
|
1961
|
+
db.insert(bingCoverageSnapshots).values({ id: `${prefix}-bing-coverage-${date}`, projectId: project.id, syncRunId, date, indexed: 48 + i, notIndexed: 3, unknown: 2, createdAt: at(date) }).run();
|
|
1962
|
+
}
|
|
1963
|
+
db.insert(gscDataWatermarks).values({ projectId: project.id, dataThroughDate: dates.at(-1), syncedThroughDate: dates.at(-1), updatedAt: nowIso }).run();
|
|
1964
|
+
db.insert(gscCoverageSnapshots).values({ id: `${prefix}-gsc-coverage`, projectId: project.id, syncRunId, date: dates.at(-1), indexed: 1, notIndexed: 0, unknownPages: 0, verifiedByInspection: 1, derivedFromImpressions: 0, reasonBreakdown: {}, createdAt: nowIso }).run();
|
|
1965
|
+
db.insert(gscUrlInspections).values({ id: `${prefix}-gsc-inspection`, projectId: project.id, syncRunId, url: `${root}services/`, indexingState: "INDEXING_ALLOWED", verdict: "PASS", coverageState: "Submitted and indexed", pageFetchState: "SUCCESSFUL", robotsTxtState: "ALLOWED", crawlTime: nowIso, lastCrawlResult: "SUCCESSFUL", isMobileFriendly: true, richResults: ["Sample FAQ"], referringUrls: [root], inspectedAt: nowIso, createdAt: nowIso }).run();
|
|
1966
|
+
db.insert(bingKeywordStats).values(searchTerms.map((query, i) => ({ id: `${prefix}-bing-keyword-${i}`, projectId: project.id, query, impressions: 310 - i * 70, clicks: 22 - i * 4, ctr: "0.071", averagePosition: String(6 + i), syncedAt: nowIso, createdAt: nowIso }))).run();
|
|
1967
|
+
db.insert(bingUrlInspections).values({ id: `${prefix}-bing-inspection`, projectId: project.id, url: root, httpCode: 200, inIndex: true, lastCrawledDate: dates.at(-2), inIndexDate: dates.at(-8), inspectedAt: nowIso, syncRunId, createdAt: nowIso, documentSize: 24120, anchorCount: 18, discoveryDate: dates[0] }).run();
|
|
1968
|
+
db.insert(gaMeasurementSyncStates).values({ projectId: project.id, acquisitionStatus: "ready", acquisitionSyncedAt: nowIso, leadStatus: "ready", leadSyncedAt: nowIso, leadAttributionScope: "landing-page", updatedAt: nowIso }).run();
|
|
1969
|
+
db.insert(gaTrafficSummaries).values({ id: `${prefix}-ga-summary`, projectId: project.id, periodStart: dates[0], periodEnd: dates.at(-1), totalSessions: 1610, totalOrganicSessions: 980, totalUsers: 1214, syncedAt: nowIso, syncRunId }).run();
|
|
1970
|
+
db.insert(gaTrafficWindowSummaries).values({ id: `${prefix}-ga-window`, projectId: project.id, windowKey: "30d", periodStart: dates[0], periodEnd: dates.at(-1), totalSessions: 1610, totalOrganicSessions: 980, totalDirectSessions: 284, totalUsers: 1214, syncedAt: nowIso, syncRunId }).run();
|
|
1971
|
+
const inventory = variant === 0 ? summitRoofingInventory() : harborResortsInventory();
|
|
1972
|
+
const crawl = await seedSiteCrawl(db, { project, prefix, root, crawlRunId, attemptId, nowIso, inventory });
|
|
1973
|
+
const brokenLinkSource = (crawl.findings.find((finding) => finding.sourceNodeKey?.startsWith("/guides/")) ?? crawl.findings.at(0))?.sourceUrl ?? root;
|
|
1974
|
+
seedLocalAndCommercialSignals(db, { project, prefix, root, syncRunId, campaignId, groupId, locationName, nowIso, dates, variant });
|
|
1975
|
+
if (variant === 1) {
|
|
1976
|
+
db.insert(healthSnapshots).values({ id: `${prefix}-health`, projectId: project.id, runId: syncRunId, overallCitedRate: "0.61", overallMentionRate: "0.74", totalPairs: 42, citedPairs: 26, mentionedPairs: 31, providerBreakdown: { openai: { citedRate: 0.62, mentionRate: 0.76, cited: 13, mentioned: 16, total: 21 }, perplexity: { citedRate: 0.6, mentionRate: 0.71, cited: 13, mentioned: 15, total: 21 } }, createdAt: nowIso }).run();
|
|
1977
|
+
db.insert(insights).values([
|
|
1978
|
+
{ id: `${prefix}-insight-1`, projectId: project.id, runId: syncRunId, type: "opportunity", severity: "medium", title: "Expand service comparison guidance", query: searchTerms[0], provider: "openai", recommendation: { action: "Draft a comparison section", target: `${root}services/`, reason: "Sample insight for the public dashboard." }, cause: { cause: "Sample answer-history gap", details: "This is stored sample data." }, dismissed: false, createdAt: nowIso },
|
|
1979
|
+
{ id: `${prefix}-insight-2`, projectId: project.id, runId: syncRunId, type: "persistent-gap", severity: "low", title: "Review one internal link target", query: searchTerms[1], provider: "perplexity", recommendation: { action: "Repair the illustrative link", target: brokenLinkSource, reason: "Sample finding for the public dashboard." }, cause: { cause: "Sample crawl finding", details: "This is stored sample data." }, dismissed: false, createdAt: nowIso }
|
|
1980
|
+
]).run();
|
|
1981
|
+
}
|
|
1982
|
+
db.insert(auditLog).values([
|
|
1983
|
+
{ id: `${prefix}-audit-1`, projectId: project.id, actor: "sample-data", action: "demo.seeded", entityType: "project", entityId: project.id, diff: "Synthetic public demo signals seeded; no provider was contacted.", createdAt: nowIso },
|
|
1984
|
+
{ id: `${prefix}-audit-2`, projectId: project.id, actor: "sample-data", action: "demo.site-health.snapshot", entityType: "site-crawl", entityId: crawlRunId, diff: "Synthetic completed crawl graph and findings stored.", createdAt: nowIso }
|
|
1985
|
+
]).run();
|
|
1986
|
+
}
|
|
1987
|
+
function seedLocalAndCommercialSignals(db, input) {
|
|
1988
|
+
const { project, prefix, root, syncRunId, campaignId, groupId, locationName, nowIso, dates, variant } = input;
|
|
1989
|
+
const business = variant === 0 ? { category: "Roofing contractor", campaign: "Spring roof inspection campaign" } : { category: "Resort hotel", campaign: "Spring resort stays campaign" };
|
|
1990
|
+
db.insert(gbpLocations).values({ id: `${prefix}-gbp-location`, projectId: project.id, accountName: "accounts/demo-fixture", locationName, displayName: `${project.displayName} - Sample`, primaryCategoryDisplayName: business.category, storefrontAddress: "100 Example Avenue, Demo City", websiteUri: root, placeId: `demo-place-${project.id}`, mapsUri: `https://maps.example/${project.id}`, description: "Sample listing for the public, view-only Canonry dashboard.", selected: true, syncedAt: nowIso, createdAt: nowIso, updatedAt: nowIso }).run();
|
|
1991
|
+
db.insert(gbpDailyMetrics).values(dates.slice(-7).flatMap((date, i) => [
|
|
1992
|
+
{ id: `${prefix}-gbp-impressions-${date}`, projectId: project.id, locationName, date, metric: "BUSINESS_IMPRESSIONS_DESKTOP_MAPS", value: 44 + i * 2, syncRunId },
|
|
1993
|
+
{ id: `${prefix}-gbp-clicks-${date}`, projectId: project.id, locationName, date, metric: "WEBSITE_CLICKS", value: 7 + i % 3, syncRunId }
|
|
1994
|
+
])).run();
|
|
1995
|
+
db.insert(gbpKeywordImpressions).values({ id: `${prefix}-gbp-keyword`, projectId: project.id, locationName, periodStart: dates[0].slice(0, 7), periodEnd: dates.at(-1).slice(0, 7), keyword: "sample local service", valueCount: 148, valueThreshold: null, syncRunId }).run();
|
|
1996
|
+
db.insert(gbpKeywordMonthly).values({ id: `${prefix}-gbp-monthly`, projectId: project.id, locationName, month: dates.at(-1).slice(0, 7), keyword: "sample local service", valueCount: 148, valueThreshold: null, syncRunId, syncedAt: nowIso }).run();
|
|
1997
|
+
db.insert(gbpPlaceActions).values({ id: `${prefix}-gbp-action`, projectId: project.id, locationName, placeActionLinkName: `${locationName}/placeActionLinks/demo`, placeActionType: "APPOINTMENT", uri: `${root}contact/`, isPreferred: true, providerType: "MERCHANT", syncRunId }).run();
|
|
1998
|
+
db.insert(backlinkSummaries).values({ id: `${prefix}-backlinks`, projectId: project.id, source: "commoncrawl", release: "DEMO-2026-09", targetDomain: project.domain, totalLinkingDomains: 3, totalHosts: 12, top10HostsShare: "0.42", queriedAt: nowIso, createdAt: nowIso }).run();
|
|
1999
|
+
db.insert(backlinkDomains).values(["partners.demo.example", "local-guides.demo.example", "trade-directory.demo.example"].map((linkingDomain, i) => ({ id: `${prefix}-backlink-domain-${i}`, projectId: project.id, source: "commoncrawl", release: "DEMO-2026-09", targetDomain: project.domain, linkingDomain, numHosts: 5 - i, createdAt: nowIso }))).run();
|
|
2000
|
+
db.insert(adsConnections).values({ id: `${prefix}-ads-connection`, projectId: project.id, adAccountId: `demo-account-${project.id}`, displayName: "Sample Ad Account", currencyCode: "USD", timezone: "UTC", status: "active", reviewStatus: "approved", integrityReviewStatus: "sample", integrityDecision: "synthetic", lastSyncedAt: nowIso, conversionTrackingConfigured: true, createdAt: nowIso, updatedAt: nowIso }).run();
|
|
2001
|
+
db.insert(adsCampaigns).values({ id: campaignId, projectId: project.id, name: business.campaign, description: "Sample stored campaign for the public dashboard.", status: "ACTIVE", biddingType: "MAXIMIZE_CONVERSIONS", dailySpendLimitMicros: 45e6, conversionEventSettingIds: ["demo-conversion-setting"], targeting: { sampleData: true }, syncRunId, syncedAt: nowIso }).run();
|
|
2002
|
+
db.insert(adsAdGroups).values({ id: groupId, projectId: project.id, campaignId, name: "Service comparison intent", description: "Sample stored ad group.", status: "ACTIVE", billingEventType: "CPC", maxBidMicros: 32e5, contextHints: ["sample service\nsample comparison"], syncRunId, syncedAt: nowIso }).run();
|
|
2003
|
+
db.insert(adsAds).values({ id: `${prefix}-ad`, projectId: project.id, adGroupId: groupId, name: "Sample responsive ad", status: "ACTIVE", creative: { sampleData: true, headline: "Sample service guidance" }, reviewStatus: "APPROVED", syncRunId, syncedAt: nowIso }).run();
|
|
2004
|
+
db.insert(adsInsightsDaily).values(dates.slice(-7).map((date, i) => ({ id: `${prefix}-ads-insight-${date}`, projectId: project.id, level: "campaign", entityId: campaignId, date, impressions: 340 + i * 20, clicks: 22 + i, spendMicros: 18e5 + i * 1e5, conversions: 2 + i % 3, syncRunId }))).run();
|
|
2005
|
+
db.insert(conversionTrackingContracts).values({ id: `${prefix}-conversion`, projectId: project.id, name: "Sample lead", eventName: "generate_lead", googleAds: { customerId: "demo-customer", conversionActionId: "demo-action", conversionId: "AW-123456", conversionLabel: "demo-label", campaignIds: [campaignId], requireBiddableGoal: true, requirePrimaryAction: true }, gtm: { accountId: "demo-account", containerId: "demo-container", tagId: "demo-tag", triggerIds: ["demo-trigger"], variableIds: ["demo-value", "demo-currency"] }, runtime: { verificationRequired: true, requireTransactionId: true, requireValue: true, requireCurrency: true, productionHosts: [project.domain] }, createdAt: nowIso, updatedAt: nowIso }).run();
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// src/demo/stores.ts
|
|
2009
|
+
import { and as and3, eq as eq4 } from "drizzle-orm";
|
|
2010
|
+
function refuseMutation() {
|
|
2011
|
+
throw new Error("The public demo stores are view only.");
|
|
2012
|
+
}
|
|
2013
|
+
function demoReadOptions(db, context) {
|
|
2014
|
+
const mapGoogle = (row) => ({
|
|
2015
|
+
domain: row.domain,
|
|
2016
|
+
connectionType: row.connectionType,
|
|
2017
|
+
propertyId: row.propertyId,
|
|
2018
|
+
createdByProjectId: row.createdByProjectId,
|
|
2019
|
+
createdAt: row.createdAt,
|
|
2020
|
+
updatedAt: row.updatedAt,
|
|
2021
|
+
scopes: []
|
|
2022
|
+
});
|
|
2023
|
+
return {
|
|
2024
|
+
// Required to register stored Google readers. No OAuth callbacks are
|
|
2025
|
+
// allowed, and no Google client id, secret, token, or transport is wired.
|
|
2026
|
+
googleStateSecret: "public-demo-no-oauth-callbacks",
|
|
2027
|
+
googleConnectionStore: {
|
|
2028
|
+
listConnections: (domain) => db.select().from(googleConnections).where(eq4(googleConnections.domain, domain)).all().map(mapGoogle),
|
|
2029
|
+
getConnection: (domain, type) => {
|
|
2030
|
+
const row = db.select().from(googleConnections).where(and3(eq4(googleConnections.domain, domain), eq4(googleConnections.connectionType, type))).get();
|
|
2031
|
+
return row ? mapGoogle(row) : void 0;
|
|
2032
|
+
},
|
|
2033
|
+
upsertConnection: refuseMutation,
|
|
2034
|
+
updateConnection: refuseMutation,
|
|
2035
|
+
deleteConnection: refuseMutation
|
|
2036
|
+
},
|
|
2037
|
+
bingConnectionStore: {
|
|
2038
|
+
getConnection: (domain) => {
|
|
2039
|
+
const row = db.select().from(bingConnections).where(eq4(bingConnections.domain, domain)).get();
|
|
2040
|
+
return row ? { domain, siteUrl: row.siteUrl, apiKey: "", createdAt: row.createdAt, updatedAt: row.updatedAt } : void 0;
|
|
2041
|
+
},
|
|
2042
|
+
upsertConnection: refuseMutation,
|
|
2043
|
+
updateConnection: refuseMutation,
|
|
2044
|
+
deleteConnection: refuseMutation
|
|
2045
|
+
},
|
|
2046
|
+
ga4CredentialStore: {
|
|
2047
|
+
getConnection: (projectName) => {
|
|
2048
|
+
const project = [context.simple, context.portfolio].find((item) => item.name === projectName);
|
|
2049
|
+
return project ? { projectName, propertyId: "demo-property", clientEmail: "sample@analytics.example", privateKey: "", createdAt: context.now.toISOString(), updatedAt: context.now.toISOString() } : void 0;
|
|
2050
|
+
},
|
|
2051
|
+
upsertConnection: refuseMutation,
|
|
2052
|
+
deleteConnection: refuseMutation
|
|
2053
|
+
},
|
|
2054
|
+
googleMarketingCredentialStore: {
|
|
2055
|
+
get: (project, provider) => {
|
|
2056
|
+
const knownProject = [context.simple, context.portfolio].some((item) => item.id === project.id && item.name === project.name);
|
|
2057
|
+
if (!knownProject) return void 0;
|
|
2058
|
+
return {
|
|
2059
|
+
// This only satisfies the status route's connection-state seam. It
|
|
2060
|
+
// is deliberately unusable for OAuth or any provider request.
|
|
2061
|
+
accessToken: `demo-status-only-${provider}-not-a-real-token`,
|
|
2062
|
+
refreshToken: null,
|
|
2063
|
+
expiresAt: null,
|
|
2064
|
+
scopes: [],
|
|
2065
|
+
developerToken: null,
|
|
2066
|
+
createdAt: context.now.toISOString(),
|
|
2067
|
+
updatedAt: context.now.toISOString()
|
|
2068
|
+
};
|
|
2069
|
+
},
|
|
2070
|
+
upsert: refuseMutation,
|
|
2071
|
+
delete: refuseMutation,
|
|
2072
|
+
hasGoogleAdsDeveloperToken: () => false
|
|
2073
|
+
},
|
|
2074
|
+
assessConversionTrackingIntegrity: ({ contract, googleAdsSnapshot, gtmSnapshot }) => assessConversionTrackingIntegrity({
|
|
2075
|
+
contract,
|
|
2076
|
+
googleAdsInventory: googleAdsSnapshot?.payload.kind === "inventory" ? googleAdsSnapshot.payload.data : null,
|
|
2077
|
+
googleAdsEvidenceId: googleAdsSnapshot?.metadata.id,
|
|
2078
|
+
gtmLiveGraph: gtmSnapshot?.payload.kind === "container" ? gtmSnapshot.payload.data.live : gtmSnapshot?.payload.kind === "live" ? gtmSnapshot.payload.data : null,
|
|
2079
|
+
gtmEvidenceId: gtmSnapshot?.metadata.id,
|
|
2080
|
+
evaluatedAt: context.now.toISOString()
|
|
2081
|
+
}),
|
|
2082
|
+
getBacklinksStatus: () => ({ duckdbInstalled: false, duckdbSpec: "Demo uses stored sample backlinks", pluginDir: "" }),
|
|
2083
|
+
listCachedReleases: () => [{ release: "DEMO-2026-09", syncStatus: "ready", bytes: 0, lastUsedAt: context.now.toISOString() }]
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// src/demo/types.ts
|
|
2088
|
+
function createDemoSeedContext(now = /* @__PURE__ */ new Date()) {
|
|
2089
|
+
return {
|
|
2090
|
+
now,
|
|
2091
|
+
simple: { id: "demo-project-summit", name: "summit-roofing", displayName: "Summit Roofing", domain: "summit-roofing.example" },
|
|
2092
|
+
portfolio: { id: "demo-project-harbor", name: "harbor-resorts", displayName: "Harbor Resorts", domain: "harbor-resorts.example" }
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/demo-server.ts
|
|
2097
|
+
async function createDemoServer(options = {}) {
|
|
2098
|
+
const db = createClient(":memory:");
|
|
2099
|
+
try {
|
|
2100
|
+
migrate(db);
|
|
2101
|
+
const context = createDemoSeedContext(options.now);
|
|
2102
|
+
seedDemoCore(db, context);
|
|
2103
|
+
await seedDemoSignals(db, context);
|
|
2104
|
+
seedDemoExploration(db, context);
|
|
2105
|
+
seedDemoMarketing(db, context);
|
|
2106
|
+
const app = await createDemoHttpServer({
|
|
2107
|
+
db,
|
|
2108
|
+
now: context.now,
|
|
2109
|
+
trustProxy: options.trustProxy,
|
|
2110
|
+
assetsDir: options.assetsDir ?? join2(dirname(fileURLToPath(import.meta.url)), "..", "assets"),
|
|
2111
|
+
readOptions: demoReadOptions(db, context)
|
|
2112
|
+
});
|
|
2113
|
+
db.$client.pragma("query_only = ON");
|
|
2114
|
+
app.addHook("onClose", async () => {
|
|
2115
|
+
db.$client.close();
|
|
2116
|
+
});
|
|
2117
|
+
return app;
|
|
2118
|
+
} catch (error) {
|
|
2119
|
+
db.$client.close();
|
|
2120
|
+
throw error;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
export {
|
|
2124
|
+
createDemoServer
|
|
2125
|
+
};
|