@ainyc/canonry 4.12.1 → 4.13.3
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/assets/index-C3thP3DI.js +302 -0
- package/assets/assets/index-D0EPNRDs.css +1 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-LNRDWAG3.js → chunk-5NYG5EC7.js} +1 -1
- package/dist/{chunk-YDGT5CAY.js → chunk-6QTH5NS5.js} +71 -4
- package/dist/{chunk-DCE3B6KD.js → chunk-7HBZCGRL.js} +18 -2
- package/dist/{chunk-L4KKHRVQ.js → chunk-FRDVC2XF.js} +538 -128
- package/dist/cli.js +23 -23
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-NT24OLLA.js → intelligence-service-BCKXIKIL.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +6 -6
- package/assets/assets/index-CCC1E6ji.js +0 -302
- package/assets/assets/index-CGXCbiM_.css +0 -1
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
configExists,
|
|
5
5
|
loadConfig,
|
|
6
6
|
saveConfigPatch
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-5NYG5EC7.js";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
10
10
|
IntelligenceService,
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
schedules,
|
|
66
66
|
trafficSources,
|
|
67
67
|
usageCounters
|
|
68
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-7HBZCGRL.js";
|
|
69
69
|
import {
|
|
70
70
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
71
71
|
AGENT_PROVIDER_IDS,
|
|
@@ -108,6 +108,11 @@ import {
|
|
|
108
108
|
emptyCitationVisibility,
|
|
109
109
|
extractAnswerMentions,
|
|
110
110
|
findDuplicateLocationLabels,
|
|
111
|
+
formatDate,
|
|
112
|
+
formatDateRange,
|
|
113
|
+
formatIsoDate,
|
|
114
|
+
formatNumber,
|
|
115
|
+
formatRatio,
|
|
111
116
|
getProviderLocationHandling,
|
|
112
117
|
hasLocationLabel,
|
|
113
118
|
internalError,
|
|
@@ -146,7 +151,7 @@ import {
|
|
|
146
151
|
visibilityStateFromAnswerMentioned,
|
|
147
152
|
windowCutoff,
|
|
148
153
|
wordpressEnvSchema
|
|
149
|
-
} from "./chunk-
|
|
154
|
+
} from "./chunk-6QTH5NS5.js";
|
|
150
155
|
|
|
151
156
|
// src/telemetry.ts
|
|
152
157
|
import crypto from "crypto";
|
|
@@ -2584,16 +2589,6 @@ var COLORS = {
|
|
|
2584
2589
|
function escapeHtml(value) {
|
|
2585
2590
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2586
2591
|
}
|
|
2587
|
-
function formatRatio(value) {
|
|
2588
|
-
if (!Number.isFinite(value) || value === 0) return "0%";
|
|
2589
|
-
return `${(value * 100).toFixed(1)}%`;
|
|
2590
|
-
}
|
|
2591
|
-
function formatNumber(value) {
|
|
2592
|
-
if (!Number.isFinite(value)) return "\u2014";
|
|
2593
|
-
if (Math.abs(value) >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
2594
|
-
if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
2595
|
-
return value.toLocaleString("en-US");
|
|
2596
|
-
}
|
|
2597
2592
|
function summarizeQueryParams(params) {
|
|
2598
2593
|
const keys = Array.from(params.keys());
|
|
2599
2594
|
const total = keys.length;
|
|
@@ -2636,23 +2631,6 @@ function formatLandingPageHtml(raw) {
|
|
|
2636
2631
|
if (!summary) return pathHtml;
|
|
2637
2632
|
return `${pathHtml}<span class="page-query" title="${escapeHtml(value)}">${escapeHtml(summary)}</span>`;
|
|
2638
2633
|
}
|
|
2639
|
-
function formatDate(iso) {
|
|
2640
|
-
if (!iso) return "\u2014";
|
|
2641
|
-
try {
|
|
2642
|
-
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
2643
|
-
const options = { month: "short", day: "numeric", year: "numeric" };
|
|
2644
|
-
const d = dateOnly && dateOnly[1] && dateOnly[2] && dateOnly[3] ? new Date(Date.UTC(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]))) : new Date(iso);
|
|
2645
|
-
if (Number.isNaN(d.getTime())) return iso;
|
|
2646
|
-
return d.toLocaleDateString("en-US", dateOnly ? { ...options, timeZone: "UTC" } : options);
|
|
2647
|
-
} catch {
|
|
2648
|
-
return iso;
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
function formatDateRange(start, end) {
|
|
2652
|
-
if (!start && !end) return "";
|
|
2653
|
-
if (start && end) return `${formatDate(start)} \u2192 ${formatDate(end)}`;
|
|
2654
|
-
return formatDate(start || end);
|
|
2655
|
-
}
|
|
2656
2634
|
function gscDateRange(report) {
|
|
2657
2635
|
const summary = report.executiveSummary.gsc;
|
|
2658
2636
|
const gsc = report.gsc;
|
|
@@ -2663,6 +2641,47 @@ function gscDateRange(report) {
|
|
|
2663
2641
|
function pluralize(count, singular, plural = `${singular}s`) {
|
|
2664
2642
|
return count === 1 ? singular : plural;
|
|
2665
2643
|
}
|
|
2644
|
+
var PROVIDER_DISPLAY_NAMES = {
|
|
2645
|
+
gemini: "Gemini",
|
|
2646
|
+
openai: "ChatGPT",
|
|
2647
|
+
claude: "Claude",
|
|
2648
|
+
perplexity: "Perplexity",
|
|
2649
|
+
local: "Local model",
|
|
2650
|
+
"cdp:chatgpt": "ChatGPT (browser)"
|
|
2651
|
+
};
|
|
2652
|
+
function providerDisplayName(name) {
|
|
2653
|
+
return PROVIDER_DISPLAY_NAMES[name] ?? name.charAt(0).toUpperCase() + name.slice(1);
|
|
2654
|
+
}
|
|
2655
|
+
function clientHorizonLabel(horizon) {
|
|
2656
|
+
switch (horizon) {
|
|
2657
|
+
case "immediate":
|
|
2658
|
+
return "Do now";
|
|
2659
|
+
case "short-term":
|
|
2660
|
+
return "This month";
|
|
2661
|
+
case "medium-term":
|
|
2662
|
+
return "Next quarter";
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
function clientConfidenceLabel(confidence) {
|
|
2666
|
+
switch (confidence) {
|
|
2667
|
+
case "high":
|
|
2668
|
+
return "Strong evidence";
|
|
2669
|
+
case "medium":
|
|
2670
|
+
return "Some evidence";
|
|
2671
|
+
case "low":
|
|
2672
|
+
return "Worth trying";
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
function clientTrendCopy(delta) {
|
|
2676
|
+
if (!delta) return null;
|
|
2677
|
+
if (delta.direction === "up") {
|
|
2678
|
+
return { text: `Up ${delta.deltaAbs.toFixed(1)} points since last check (was ${delta.prior}%)`, tone: "positive", arrow: "\u2191" };
|
|
2679
|
+
}
|
|
2680
|
+
if (delta.direction === "down") {
|
|
2681
|
+
return { text: `Down ${Math.abs(delta.deltaAbs).toFixed(1)} points since last check (was ${delta.prior}%)`, tone: "negative", arrow: "\u2193" };
|
|
2682
|
+
}
|
|
2683
|
+
return { text: `Holding steady since last check (was ${delta.prior}%)`, tone: "neutral", arrow: "\u2192" };
|
|
2684
|
+
}
|
|
2666
2685
|
function compactInlineList(items, limit = 3) {
|
|
2667
2686
|
const visible = items.slice(0, limit);
|
|
2668
2687
|
const more = items.length - visible.length;
|
|
@@ -3192,6 +3211,222 @@ table.report-table td .badge {
|
|
|
3192
3211
|
color: ${COLORS.textFaint};
|
|
3193
3212
|
font-size: 12px;
|
|
3194
3213
|
}
|
|
3214
|
+
.client-hero {
|
|
3215
|
+
background: ${COLORS.surface};
|
|
3216
|
+
border: 1px solid ${COLORS.border};
|
|
3217
|
+
border-radius: 16px;
|
|
3218
|
+
padding: 32px;
|
|
3219
|
+
margin-bottom: 24px;
|
|
3220
|
+
}
|
|
3221
|
+
.client-hero .client-hero-eyebrow {
|
|
3222
|
+
text-transform: uppercase;
|
|
3223
|
+
letter-spacing: 0.05em;
|
|
3224
|
+
font-size: 11px;
|
|
3225
|
+
font-weight: 600;
|
|
3226
|
+
color: ${COLORS.textFaint};
|
|
3227
|
+
}
|
|
3228
|
+
.client-hero .client-hero-number {
|
|
3229
|
+
font-size: 80px;
|
|
3230
|
+
line-height: 1;
|
|
3231
|
+
font-weight: 800;
|
|
3232
|
+
letter-spacing: -0.02em;
|
|
3233
|
+
color: ${COLORS.text};
|
|
3234
|
+
margin: 14px 0 18px;
|
|
3235
|
+
}
|
|
3236
|
+
.client-hero .client-hero-sentence {
|
|
3237
|
+
font-size: 17px;
|
|
3238
|
+
color: #d4d4d8;
|
|
3239
|
+
max-width: 720px;
|
|
3240
|
+
margin: 0;
|
|
3241
|
+
}
|
|
3242
|
+
.client-hero .client-hero-trend {
|
|
3243
|
+
margin-top: 14px;
|
|
3244
|
+
font-size: 14px;
|
|
3245
|
+
font-weight: 500;
|
|
3246
|
+
}
|
|
3247
|
+
.client-hero .client-hero-trend.tone-positive { color: ${COLORS.positive}; }
|
|
3248
|
+
.client-hero .client-hero-trend.tone-negative { color: ${COLORS.negative}; }
|
|
3249
|
+
.client-hero .client-hero-trend.tone-neutral { color: ${COLORS.textMuted}; }
|
|
3250
|
+
.client-metric-grid {
|
|
3251
|
+
display: grid;
|
|
3252
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
3253
|
+
gap: 16px;
|
|
3254
|
+
margin-bottom: 24px;
|
|
3255
|
+
}
|
|
3256
|
+
.client-metric-tile {
|
|
3257
|
+
background: ${COLORS.surface};
|
|
3258
|
+
border: 1px solid ${COLORS.border};
|
|
3259
|
+
border-radius: 12px;
|
|
3260
|
+
padding: 22px 24px;
|
|
3261
|
+
}
|
|
3262
|
+
.client-metric-tile .label {
|
|
3263
|
+
text-transform: uppercase;
|
|
3264
|
+
letter-spacing: 0.05em;
|
|
3265
|
+
font-size: 11px;
|
|
3266
|
+
font-weight: 600;
|
|
3267
|
+
color: ${COLORS.textFaint};
|
|
3268
|
+
margin-bottom: 14px;
|
|
3269
|
+
}
|
|
3270
|
+
.client-metric-tile .value {
|
|
3271
|
+
font-size: 48px;
|
|
3272
|
+
line-height: 1;
|
|
3273
|
+
font-weight: 800;
|
|
3274
|
+
letter-spacing: -0.02em;
|
|
3275
|
+
color: ${COLORS.text};
|
|
3276
|
+
}
|
|
3277
|
+
.client-metric-tile .subtitle {
|
|
3278
|
+
margin-top: 10px;
|
|
3279
|
+
font-size: 12px;
|
|
3280
|
+
color: ${COLORS.textMuted};
|
|
3281
|
+
}
|
|
3282
|
+
.client-card {
|
|
3283
|
+
background: ${COLORS.surface};
|
|
3284
|
+
border: 1px solid ${COLORS.border};
|
|
3285
|
+
border-radius: 12px;
|
|
3286
|
+
padding: 22px 24px;
|
|
3287
|
+
margin-bottom: 16px;
|
|
3288
|
+
}
|
|
3289
|
+
.client-card h3 {
|
|
3290
|
+
font-size: 15px;
|
|
3291
|
+
font-weight: 600;
|
|
3292
|
+
margin: 0 0 4px;
|
|
3293
|
+
}
|
|
3294
|
+
.client-card .card-subtitle {
|
|
3295
|
+
font-size: 12px;
|
|
3296
|
+
color: ${COLORS.textMuted};
|
|
3297
|
+
margin: 0 0 18px;
|
|
3298
|
+
}
|
|
3299
|
+
.client-bar-list {
|
|
3300
|
+
display: flex;
|
|
3301
|
+
flex-direction: column;
|
|
3302
|
+
gap: 14px;
|
|
3303
|
+
}
|
|
3304
|
+
.client-bar-row {
|
|
3305
|
+
display: grid;
|
|
3306
|
+
grid-template-columns: 140px 1fr 130px;
|
|
3307
|
+
align-items: center;
|
|
3308
|
+
gap: 14px;
|
|
3309
|
+
font-size: 13px;
|
|
3310
|
+
}
|
|
3311
|
+
.client-bar-row .bar-label { color: #d4d4d8; }
|
|
3312
|
+
.client-bar-row .bar-track {
|
|
3313
|
+
height: 10px;
|
|
3314
|
+
background: ${COLORS.border};
|
|
3315
|
+
border-radius: 999px;
|
|
3316
|
+
overflow: hidden;
|
|
3317
|
+
}
|
|
3318
|
+
.client-bar-row .bar-fill {
|
|
3319
|
+
height: 100%;
|
|
3320
|
+
border-radius: 999px;
|
|
3321
|
+
background: ${COLORS.positive}b3;
|
|
3322
|
+
}
|
|
3323
|
+
.client-bar-row .bar-fill.bar-fill-neutral { background: #a1a1aaaa; }
|
|
3324
|
+
.client-bar-row .bar-fill.bar-fill-sky { background: #38bdf8b3; }
|
|
3325
|
+
.client-bar-row .bar-value {
|
|
3326
|
+
text-align: right;
|
|
3327
|
+
font-size: 13px;
|
|
3328
|
+
font-weight: 600;
|
|
3329
|
+
color: ${COLORS.text};
|
|
3330
|
+
font-variant-numeric: tabular-nums;
|
|
3331
|
+
}
|
|
3332
|
+
.client-bar-row .bar-value-sub { color: ${COLORS.textFaint}; font-weight: 400; }
|
|
3333
|
+
.client-progress-number {
|
|
3334
|
+
font-size: 56px;
|
|
3335
|
+
font-weight: 800;
|
|
3336
|
+
line-height: 1;
|
|
3337
|
+
letter-spacing: -0.02em;
|
|
3338
|
+
margin: 12px 0 4px;
|
|
3339
|
+
}
|
|
3340
|
+
.client-progress-number.tone-positive { color: ${COLORS.positive}; }
|
|
3341
|
+
.client-progress-number.tone-caution { color: ${COLORS.caution}; }
|
|
3342
|
+
.client-progress-number.tone-negative { color: ${COLORS.negative}; }
|
|
3343
|
+
.client-progress-bar {
|
|
3344
|
+
height: 12px;
|
|
3345
|
+
background: ${COLORS.border};
|
|
3346
|
+
border-radius: 999px;
|
|
3347
|
+
overflow: hidden;
|
|
3348
|
+
margin: 12px 0 14px;
|
|
3349
|
+
}
|
|
3350
|
+
.client-progress-fill { height: 100%; border-radius: 999px; }
|
|
3351
|
+
.client-progress-fill.tone-positive { background: ${COLORS.positive}b3; }
|
|
3352
|
+
.client-progress-fill.tone-caution { background: ${COLORS.caution}b3; }
|
|
3353
|
+
.client-progress-fill.tone-negative { background: ${COLORS.negative}b3; }
|
|
3354
|
+
.client-evidence-grid {
|
|
3355
|
+
display: grid;
|
|
3356
|
+
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
|
3357
|
+
gap: 16px;
|
|
3358
|
+
}
|
|
3359
|
+
.client-opportunity-list {
|
|
3360
|
+
display: flex;
|
|
3361
|
+
flex-direction: column;
|
|
3362
|
+
gap: 8px;
|
|
3363
|
+
margin: 0;
|
|
3364
|
+
padding: 0;
|
|
3365
|
+
list-style: none;
|
|
3366
|
+
}
|
|
3367
|
+
.client-opportunity-list li {
|
|
3368
|
+
background: #09090b;
|
|
3369
|
+
border: 1px solid ${COLORS.border};
|
|
3370
|
+
border-radius: 8px;
|
|
3371
|
+
padding: 10px 14px;
|
|
3372
|
+
}
|
|
3373
|
+
.client-opportunity-list li .op-query {
|
|
3374
|
+
font-weight: 500;
|
|
3375
|
+
color: ${COLORS.text};
|
|
3376
|
+
font-size: 13px;
|
|
3377
|
+
}
|
|
3378
|
+
.client-opportunity-list li .op-action {
|
|
3379
|
+
margin-top: 2px;
|
|
3380
|
+
font-size: 11px;
|
|
3381
|
+
color: ${COLORS.textMuted};
|
|
3382
|
+
}
|
|
3383
|
+
.client-confidence-note {
|
|
3384
|
+
background: ${COLORS.surface};
|
|
3385
|
+
border: 1px solid ${COLORS.border};
|
|
3386
|
+
border-radius: 8px;
|
|
3387
|
+
padding: 10px 14px;
|
|
3388
|
+
font-size: 12px;
|
|
3389
|
+
color: ${COLORS.textMuted};
|
|
3390
|
+
margin-bottom: 6px;
|
|
3391
|
+
}
|
|
3392
|
+
.client-explainer {
|
|
3393
|
+
background: #09090b;
|
|
3394
|
+
border: 1px solid ${COLORS.border};
|
|
3395
|
+
border-radius: 12px;
|
|
3396
|
+
padding: 12px 16px;
|
|
3397
|
+
font-size: 12px;
|
|
3398
|
+
color: ${COLORS.textMuted};
|
|
3399
|
+
margin-bottom: 16px;
|
|
3400
|
+
line-height: 1.6;
|
|
3401
|
+
}
|
|
3402
|
+
.client-explainer strong { color: ${COLORS.text}; }
|
|
3403
|
+
.client-explainer .term { color: #d4d4d8; font-weight: 500; }
|
|
3404
|
+
.client-questions-list {
|
|
3405
|
+
display: grid;
|
|
3406
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
3407
|
+
gap: 8px;
|
|
3408
|
+
margin: 0;
|
|
3409
|
+
padding: 0;
|
|
3410
|
+
list-style: none;
|
|
3411
|
+
}
|
|
3412
|
+
.client-questions-list li {
|
|
3413
|
+
display: flex;
|
|
3414
|
+
align-items: flex-start;
|
|
3415
|
+
gap: 12px;
|
|
3416
|
+
background: #09090b;
|
|
3417
|
+
border: 1px solid ${COLORS.border};
|
|
3418
|
+
border-radius: 8px;
|
|
3419
|
+
padding: 10px 14px;
|
|
3420
|
+
font-size: 13px;
|
|
3421
|
+
color: #d4d4d8;
|
|
3422
|
+
}
|
|
3423
|
+
.client-questions-list li .qnum {
|
|
3424
|
+
flex-shrink: 0;
|
|
3425
|
+
font-size: 11px;
|
|
3426
|
+
font-weight: 600;
|
|
3427
|
+
color: ${COLORS.textFaint};
|
|
3428
|
+
font-variant-numeric: tabular-nums;
|
|
3429
|
+
}
|
|
3195
3430
|
@media (max-width: 760px) {
|
|
3196
3431
|
.container { padding: 32px 16px 72px; }
|
|
3197
3432
|
.executive-hero { grid-template-columns: 1fr; }
|
|
@@ -3199,10 +3434,37 @@ table.report-table td .badge {
|
|
|
3199
3434
|
.source-bar-row { grid-template-columns: 1fr; gap: 6px; }
|
|
3200
3435
|
.source-bar-value { text-align: left; }
|
|
3201
3436
|
.chart-grid { grid-template-columns: 1fr; }
|
|
3437
|
+
.client-hero .client-hero-number { font-size: 56px; }
|
|
3438
|
+
.client-metric-tile .value { font-size: 36px; }
|
|
3439
|
+
.client-bar-row { grid-template-columns: 100px 1fr 100px; gap: 10px; }
|
|
3202
3440
|
}
|
|
3203
3441
|
@media print {
|
|
3204
|
-
|
|
3205
|
-
|
|
3442
|
+
@page { margin: 0.5in; }
|
|
3443
|
+
html, body {
|
|
3444
|
+
background: ${COLORS.bg};
|
|
3445
|
+
color: ${COLORS.text};
|
|
3446
|
+
-webkit-print-color-adjust: exact;
|
|
3447
|
+
print-color-adjust: exact;
|
|
3448
|
+
}
|
|
3449
|
+
.container { max-width: none; padding: 0; }
|
|
3450
|
+
section.report-section,
|
|
3451
|
+
.executive-hero,
|
|
3452
|
+
.headline-card,
|
|
3453
|
+
.hero-proof,
|
|
3454
|
+
.client-hero,
|
|
3455
|
+
.client-metric-tile,
|
|
3456
|
+
.client-card,
|
|
3457
|
+
.client-note,
|
|
3458
|
+
.chart-card,
|
|
3459
|
+
.action-card,
|
|
3460
|
+
.insight-card,
|
|
3461
|
+
.source-bar-row,
|
|
3462
|
+
.client-bar-row,
|
|
3463
|
+
tr,
|
|
3464
|
+
table { break-inside: avoid; }
|
|
3465
|
+
h1, h2, h3, .eyebrow { break-after: avoid; }
|
|
3466
|
+
.footer { margin-top: 32px; }
|
|
3467
|
+
.footer a { color: ${COLORS.text}; }
|
|
3206
3468
|
}
|
|
3207
3469
|
`;
|
|
3208
3470
|
function section(opts, body) {
|
|
@@ -3389,68 +3651,82 @@ function renderTrafficDeltaTile(label, delta, countLabel) {
|
|
|
3389
3651
|
</div>`;
|
|
3390
3652
|
}
|
|
3391
3653
|
var WHATS_CHANGED_PERIOD_DAYS = 14;
|
|
3392
|
-
function renderProviderMovements(movements) {
|
|
3654
|
+
function renderProviderMovements(movements, audience) {
|
|
3393
3655
|
const meaningful = movements.filter((m) => m.direction !== "flat");
|
|
3394
3656
|
if (meaningful.length === 0) return "";
|
|
3657
|
+
const isClient = audience === "client";
|
|
3395
3658
|
const rows = meaningful.map((m) => {
|
|
3396
3659
|
const sign = m.deltaAbs > 0 ? "+" : "";
|
|
3397
3660
|
return `<tr>
|
|
3398
|
-
<td>${escapeHtml(m.provider)}</td>
|
|
3661
|
+
<td>${escapeHtml(isClient ? providerDisplayName(m.provider) : m.provider)}</td>
|
|
3399
3662
|
<td class="numeric">${m.prior}%</td>
|
|
3400
3663
|
<td class="numeric">${m.current}%</td>
|
|
3401
3664
|
<td class="numeric ${deltaToneClass(m.direction)}">${sign}${m.deltaAbs.toFixed(1)}% ${deltaArrow(m.direction)}</td>
|
|
3402
3665
|
</tr>`;
|
|
3403
3666
|
}).join("");
|
|
3404
|
-
|
|
3667
|
+
const heading = isClient ? "How each AI tool changed" : "AI engine movements";
|
|
3668
|
+
const colA = isClient ? "AI tool" : "Engine";
|
|
3669
|
+
const colB = isClient ? "Was" : "Prior";
|
|
3670
|
+
const colC = isClient ? "Now" : "Current";
|
|
3671
|
+
return `<div class="chart-card"><h3>${heading}</h3>
|
|
3405
3672
|
<table class="report-table">
|
|
3406
|
-
<thead><tr><th
|
|
3673
|
+
<thead><tr><th>${colA}</th><th class="numeric">${colB}</th><th class="numeric">${colC}</th><th class="numeric">Change</th></tr></thead>
|
|
3407
3674
|
<tbody>${rows}</tbody>
|
|
3408
3675
|
</table>
|
|
3409
3676
|
</div>`;
|
|
3410
3677
|
}
|
|
3411
|
-
function renderWinsLosses(insights2, heading, emptyMessage) {
|
|
3678
|
+
function renderWinsLosses(insights2, heading, emptyMessage, audience) {
|
|
3412
3679
|
if (insights2.length === 0) {
|
|
3413
3680
|
return `<div class="chart-card"><h3>${escapeHtml(heading)}</h3>
|
|
3414
3681
|
<p class="section-intro">${escapeHtml(emptyMessage)}</p>
|
|
3415
3682
|
</div>`;
|
|
3416
3683
|
}
|
|
3684
|
+
const isClient = audience === "client";
|
|
3417
3685
|
const rows = insights2.map((i) => {
|
|
3418
3686
|
const tone = severityTone(i.severity);
|
|
3419
3687
|
const countChip = i.instanceCount > 1 ? ` <span class="badge tone-neutral">\xD7 ${i.instanceCount}</span>` : "";
|
|
3688
|
+
const severityCell = isClient ? "" : `<td><span class="badge tone-${tone}">${escapeHtml(reportSeverityLabel(i.severity))}</span></td>`;
|
|
3420
3689
|
return `<tr>
|
|
3421
|
-
|
|
3690
|
+
${severityCell}
|
|
3422
3691
|
<td>${escapeHtml(i.title)}${countChip}</td>
|
|
3423
3692
|
<td>${escapeHtml(i.query)}</td>
|
|
3424
|
-
<td>${escapeHtml(i.provider)}</td>
|
|
3693
|
+
<td>${escapeHtml(isClient ? providerDisplayName(i.provider) : i.provider)}</td>
|
|
3425
3694
|
</tr>`;
|
|
3426
3695
|
}).join("");
|
|
3696
|
+
const headers = isClient ? `<tr><th>What changed</th><th>Customer question</th><th>AI tool</th></tr>` : `<tr><th>Severity</th><th>Title</th><th>Query</th><th>Provider</th></tr>`;
|
|
3427
3697
|
return `<div class="chart-card"><h3>${escapeHtml(heading)}</h3>
|
|
3428
3698
|
<table class="report-table">
|
|
3429
|
-
<thead
|
|
3699
|
+
<thead>${headers}</thead>
|
|
3430
3700
|
<tbody>${rows}</tbody>
|
|
3431
3701
|
</table>
|
|
3432
3702
|
</div>`;
|
|
3433
3703
|
}
|
|
3434
|
-
function renderWhatsChanged(report) {
|
|
3704
|
+
function renderWhatsChanged(report, audience) {
|
|
3435
3705
|
const w = report.whatsChanged;
|
|
3706
|
+
const isClient = audience === "client";
|
|
3707
|
+
const eyebrow = isClient ? "Since last check" : "Section 2";
|
|
3708
|
+
const title = isClient ? "What's different since last check" : "What's Changed";
|
|
3709
|
+
const intro = isClient ? "" : w.headline;
|
|
3436
3710
|
if (!w.enoughHistory && !w.gscClicksDelta && !w.aiReferralsDelta && w.wins.length === 0 && w.regressions.length === 0) {
|
|
3437
3711
|
return section(
|
|
3438
|
-
{ id: "whats-changed", eyebrow
|
|
3439
|
-
renderEmpty("Trends will appear after a few more checks.")
|
|
3712
|
+
{ id: "whats-changed", eyebrow, title, intro },
|
|
3713
|
+
renderEmpty(isClient ? "No comparison yet \u2014 trends will appear after a few more checks." : "Trends will appear after a few more checks.")
|
|
3440
3714
|
);
|
|
3441
3715
|
}
|
|
3442
3716
|
const rateTiles = `<div class="metric-grid">
|
|
3443
|
-
${renderRateDeltaTile("Citation rate", w.citationRate, "%")}
|
|
3444
|
-
${renderRateDeltaTile("Mention rate", w.mentionRate, "%")}
|
|
3445
|
-
${renderRateDeltaTile("Cited queries", w.citedQueryCount, "count")}
|
|
3446
|
-
${renderTrafficDeltaTile("GSC clicks", w.gscClicksDelta, "clicks")}
|
|
3447
|
-
${renderTrafficDeltaTile("AI referral sessions", w.aiReferralsDelta, "sessions")}
|
|
3717
|
+
${renderRateDeltaTile(isClient ? "AI links to your website" : "Citation rate", w.citationRate, "%")}
|
|
3718
|
+
${renderRateDeltaTile(isClient ? "AI mentions your name" : "Mention rate", w.mentionRate, "%")}
|
|
3719
|
+
${renderRateDeltaTile(isClient ? "Questions AI answered with you" : "Cited queries", w.citedQueryCount, "count")}
|
|
3720
|
+
${renderTrafficDeltaTile(isClient ? "Visitors from Google" : "GSC clicks", w.gscClicksDelta, isClient ? "visits" : "clicks")}
|
|
3721
|
+
${renderTrafficDeltaTile(isClient ? "Visitors from AI tools" : "AI referral sessions", w.aiReferralsDelta, isClient ? "visits" : "sessions")}
|
|
3448
3722
|
</div>`;
|
|
3449
|
-
const movements = renderProviderMovements(w.providerMovements);
|
|
3450
|
-
const
|
|
3451
|
-
const
|
|
3723
|
+
const movements = renderProviderMovements(w.providerMovements, audience);
|
|
3724
|
+
const winsHeading = isClient ? "What got better" : "Wins";
|
|
3725
|
+
const lossesHeading = isClient ? "What got worse" : "Regressions";
|
|
3726
|
+
const wins = renderWinsLosses(w.wins, winsHeading, isClient ? "No new wins this period." : "No new gains in the latest check.", audience);
|
|
3727
|
+
const regressions = renderWinsLosses(w.regressions, lossesHeading, isClient ? "Nothing got worse this period." : "No new regressions in the latest check.", audience);
|
|
3452
3728
|
return section(
|
|
3453
|
-
{ id: "whats-changed", eyebrow
|
|
3729
|
+
{ id: "whats-changed", eyebrow, title, intro },
|
|
3454
3730
|
`${rateTiles}${movements}${wins}${regressions}`
|
|
3455
3731
|
);
|
|
3456
3732
|
}
|
|
@@ -4104,8 +4380,9 @@ function renderRecommendedNextSteps(report) {
|
|
|
4104
4380
|
function actionAudienceMatches(action, audience) {
|
|
4105
4381
|
return action.audience === "both" || action.audience === audience;
|
|
4106
4382
|
}
|
|
4107
|
-
function renderActionCards(actions) {
|
|
4108
|
-
|
|
4383
|
+
function renderActionCards(actions, audience) {
|
|
4384
|
+
const isClient = audience === "client";
|
|
4385
|
+
if (actions.length === 0) return renderEmpty(isClient ? "No recommendations yet \u2014 run an AI check to populate this." : "No prioritized actions yet.");
|
|
4109
4386
|
return `<div class="action-card-grid">
|
|
4110
4387
|
${actions.map((action, idx) => {
|
|
4111
4388
|
const tone = reportActionTone(action);
|
|
@@ -4113,18 +4390,22 @@ function renderActionCards(actions) {
|
|
|
4113
4390
|
const evidence = action.evidence.length > 0 ? `<ul>${action.evidence.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : "";
|
|
4114
4391
|
const proof = renderProofChips(action.evidence.length > 0 ? action.evidence : action.why, 3);
|
|
4115
4392
|
const details = why || evidence ? `<details class="action-details">
|
|
4116
|
-
<summary
|
|
4117
|
-
${why ? `<div><strong
|
|
4118
|
-
${evidence ? `<div><strong
|
|
4393
|
+
<summary>${isClient ? "See the data behind this" : "Evidence details"}</summary>
|
|
4394
|
+
${why ? `<div><strong>${isClient ? "Why this matters" : "Why"}</strong>${why}</div>` : ""}
|
|
4395
|
+
${evidence ? `<div><strong>${isClient ? "What we saw" : "Evidence"}</strong>${evidence}</div>` : ""}
|
|
4119
4396
|
</details>` : "";
|
|
4397
|
+
const horizonLabel = isClient ? clientHorizonLabel(action.horizon) : reportHorizonLabel(action.horizon);
|
|
4398
|
+
const confidenceLabel = isClient ? clientConfidenceLabel(action.confidence) : `${reportConfidenceLabel(action.confidence)} confidence`;
|
|
4399
|
+
const categoryBadge = isClient ? "" : `<span class="badge tone-neutral">${escapeHtml(reportActionCategoryLabel(action.category))}</span>`;
|
|
4400
|
+
const successLabel = isClient ? "What success looks like:" : "Win condition:";
|
|
4120
4401
|
return `<article class="action-card">
|
|
4121
4402
|
<div class="action-head">
|
|
4122
|
-
<div class="action-rank" title="Impact rank \u2014 1 is the highest-leverage action">${idx + 1}</div>
|
|
4403
|
+
<div class="action-rank" title="${isClient ? "Priority \u2014 1 will move the needle fastest" : "Impact rank \u2014 1 is the highest-leverage action"}">${idx + 1}</div>
|
|
4123
4404
|
<div>
|
|
4124
4405
|
<div class="action-meta">
|
|
4125
|
-
<span class="badge tone-${tone}">${escapeHtml(
|
|
4126
|
-
|
|
4127
|
-
<span class="badge tone-neutral">${escapeHtml(
|
|
4406
|
+
<span class="badge tone-${tone}">${escapeHtml(horizonLabel)}</span>
|
|
4407
|
+
${categoryBadge}
|
|
4408
|
+
<span class="badge tone-neutral">${escapeHtml(confidenceLabel)}</span>
|
|
4128
4409
|
</div>
|
|
4129
4410
|
<h3>${escapeHtml(action.title)}</h3>
|
|
4130
4411
|
</div>
|
|
@@ -4132,7 +4413,7 @@ function renderActionCards(actions) {
|
|
|
4132
4413
|
<p>${escapeHtml(action.action)}</p>
|
|
4133
4414
|
${proof}
|
|
4134
4415
|
${details}
|
|
4135
|
-
<div class="success-metric"><strong
|
|
4416
|
+
<div class="success-metric"><strong>${successLabel}</strong> ${escapeHtml(action.successMetric)}</div>
|
|
4136
4417
|
</article>`;
|
|
4137
4418
|
}).join("")}
|
|
4138
4419
|
</div>`;
|
|
@@ -4143,76 +4424,150 @@ function renderAudienceActionPlan(report, audience) {
|
|
|
4143
4424
|
return section(
|
|
4144
4425
|
{
|
|
4145
4426
|
id: audience === "client" ? "client-action-plan" : "agency-action-plan",
|
|
4146
|
-
eyebrow: audience === "client" ? "
|
|
4147
|
-
title: audience === "client" ? "What
|
|
4148
|
-
intro: audience === "client" ? "
|
|
4427
|
+
eyebrow: audience === "client" ? "Action plan" : "Agency actions",
|
|
4428
|
+
title: audience === "client" ? "What to do next" : "Agency Action Plan",
|
|
4429
|
+
intro: audience === "client" ? "Approve these in order. They are sorted by what will move the needle fastest." : "The highest-leverage work, sorted by urgency and evidence strength."
|
|
4149
4430
|
},
|
|
4150
|
-
renderActionCards(actions)
|
|
4431
|
+
renderActionCards(actions, audience)
|
|
4151
4432
|
);
|
|
4152
4433
|
}
|
|
4153
4434
|
function renderClientSummary(report) {
|
|
4154
4435
|
const s = report.executiveSummary;
|
|
4155
|
-
const
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4436
|
+
const sc = report.citationScorecard;
|
|
4437
|
+
const totalQ = s.totalQueryCount;
|
|
4438
|
+
const heroNumber = totalQ > 0 ? `${s.citationRate}%` : "\u2014";
|
|
4439
|
+
const heroSentence = totalQ > 0 ? `When customers asked AI ${totalQ} ${pluralize(totalQ, "question")} about your industry, AI linked to your website in ${s.citedQueryCount} of ${totalQ === 1 ? "them" : "those answers"}.` : "No AI check has been run yet. Run a check to see how AI tools answer customer questions about your business.";
|
|
4440
|
+
const trend = clientTrendCopy(report.whatsChanged.citationRate);
|
|
4441
|
+
const heroTrend = trend ? `<p class="client-hero-trend tone-${trend.tone}"><span style="margin-right:6px;">${trend.arrow}</span>${escapeHtml(trend.text)}</p>` : "";
|
|
4442
|
+
const hero = `<div class="client-hero">
|
|
4443
|
+
<div class="client-hero-eyebrow">Overview</div>
|
|
4444
|
+
<div class="client-hero-number">${heroNumber}</div>
|
|
4445
|
+
<p class="client-hero-sentence">${escapeHtml(heroSentence)}</p>
|
|
4446
|
+
${heroTrend}
|
|
4159
4447
|
</div>`;
|
|
4160
|
-
const
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
intro: report.clientSummary.overview
|
|
4167
|
-
},
|
|
4168
|
-
`<div class="chart-card">
|
|
4169
|
-
<h3>${escapeHtml(report.clientSummary.headline)}</h3>
|
|
4170
|
-
<p class="source-origin-headline">${escapeHtml(report.clientSummary.overview)}</p>
|
|
4448
|
+
const providerSubtitle = sc.providers.length > 0 ? sc.providers.map(providerDisplayName).join(", ") : `${formatNumber(s.queryCount)} ${pluralize(s.queryCount, "question")} tested`;
|
|
4449
|
+
const tiles = `<div class="client-metric-grid">
|
|
4450
|
+
<div class="client-metric-tile">
|
|
4451
|
+
<div class="label">AI mentions your name</div>
|
|
4452
|
+
<div class="value">${s.mentionRate}%</div>
|
|
4453
|
+
<div class="subtitle">${totalQ > 0 ? `Says your name in ${s.mentionedQueryCount} of ${totalQ} ${pluralize(totalQ, "answer")}` : "No data yet"}</div>
|
|
4171
4454
|
</div>
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4455
|
+
<div class="client-metric-tile">
|
|
4456
|
+
<div class="label">AI links to your website</div>
|
|
4457
|
+
<div class="value">${s.citationRate}%</div>
|
|
4458
|
+
<div class="subtitle">${totalQ > 0 ? `Cites your site as a source in ${s.citedQueryCount} of ${totalQ} ${pluralize(totalQ, "answer")}` : "No data yet"}</div>
|
|
4459
|
+
</div>
|
|
4460
|
+
<div class="client-metric-tile">
|
|
4461
|
+
<div class="label">AI tools tested</div>
|
|
4462
|
+
<div class="value">${formatNumber(s.providerCount)}</div>
|
|
4463
|
+
<div class="subtitle">${escapeHtml(providerSubtitle)}</div>
|
|
4464
|
+
</div>
|
|
4465
|
+
</div>`;
|
|
4466
|
+
const explainer = `<div class="client-explainer">
|
|
4467
|
+
<strong>Mentions and links are different.</strong>
|
|
4468
|
+
A <span class="term">mention</span> is when AI says your name out loud in its answer.
|
|
4469
|
+
A <span class="term">link</span> is when AI lists your website as a source it used.
|
|
4470
|
+
AI can do either, both, or neither \u2014 that's why we track both.
|
|
4471
|
+
</div>`;
|
|
4472
|
+
const questions = sc.queries.length > 0 ? `<div class="client-card">
|
|
4473
|
+
<h3>Customer questions we tested</h3>
|
|
4474
|
+
<p class="card-subtitle">These are the ${sc.queries.length} ${pluralize(sc.queries.length, "question we asked", "questions we asked")} every AI tool. The numbers above measure how often you came up.</p>
|
|
4475
|
+
<ol class="client-questions-list">
|
|
4476
|
+
${sc.queries.map((q, i) => `<li><span class="qnum">${String(i + 1).padStart(2, "0")}</span><span>"${escapeHtml(q)}"</span></li>`).join("")}
|
|
4477
|
+
</ol>
|
|
4478
|
+
</div>` : "";
|
|
4479
|
+
const providerBars = sc.providerRates.length > 0 ? `<div class="client-card">
|
|
4480
|
+
<h3>How often each AI tool links to your website</h3>
|
|
4481
|
+
<p class="card-subtitle">Higher is better. Each bar shows the share of customer questions where the AI cited your site.</p>
|
|
4482
|
+
<div class="client-bar-list">
|
|
4483
|
+
${sc.providerRates.map((r) => {
|
|
4484
|
+
const pct = Math.max(r.citationRate, 1.5);
|
|
4485
|
+
return `<div class="client-bar-row">
|
|
4486
|
+
<span class="bar-label">${escapeHtml(providerDisplayName(r.provider))}</span>
|
|
4487
|
+
<div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
|
|
4488
|
+
<span class="bar-value">${r.citationRate}% <span class="bar-value-sub">(${r.citedCount}/${r.totalCount})</span></span>
|
|
4489
|
+
</div>`;
|
|
4490
|
+
}).join("")}
|
|
4491
|
+
</div>
|
|
4492
|
+
</div>` : "";
|
|
4493
|
+
const notes = report.clientSummary.confidenceNotes.length > 0 ? `<div>${report.clientSummary.confidenceNotes.map((note) => `<div class="client-confidence-note">${escapeHtml(note)}</div>`).join("")}</div>` : "";
|
|
4494
|
+
return `<section class="report-section" id="client-summary">${hero}${tiles}${explainer}${questions}${providerBars}${notes}</section>`;
|
|
4175
4495
|
}
|
|
4176
4496
|
function renderClientEvidenceSummary(report) {
|
|
4177
|
-
const
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4497
|
+
const ai = report.aiSourceOrigin.topDomains.slice(0, 5);
|
|
4498
|
+
const gsc = report.gsc;
|
|
4499
|
+
const indexing = report.indexingHealth;
|
|
4500
|
+
const opportunities = dedupeReportOpportunities(report).slice(0, 5);
|
|
4501
|
+
const aiMax = ai.length > 0 ? Math.max(...ai.map((d) => d.count)) : 0;
|
|
4502
|
+
const gscMax = gsc ? Math.max(...gsc.topQueries.slice(0, 5).map((q) => q.impressions), 1) : 0;
|
|
4503
|
+
const cards = [];
|
|
4504
|
+
if (ai.length > 0) {
|
|
4505
|
+
cards.push(`<div class="client-card">
|
|
4506
|
+
<h3>Where AI gets its answers</h3>
|
|
4507
|
+
<p class="card-subtitle">The websites AI tools cited most often when answering customer questions about your industry.</p>
|
|
4508
|
+
<div class="client-bar-list">
|
|
4509
|
+
${ai.map((d) => {
|
|
4510
|
+
const pct = aiMax > 0 ? Math.max(d.count / aiMax * 100, 1.5) : 0;
|
|
4511
|
+
const label = escapeHtml(d.domain) + (d.isCompetitor ? ' <span style="color:' + COLORS.textFaint + ';font-size:11px;">(competitor)</span>' : "");
|
|
4512
|
+
return `<div class="client-bar-row">
|
|
4513
|
+
<span class="bar-label">${label}</span>
|
|
4514
|
+
<div class="bar-track"><div class="bar-fill bar-fill-neutral" style="width:${pct}%"></div></div>
|
|
4515
|
+
<span class="bar-value">${formatNumber(d.count)}\xD7</span>
|
|
4516
|
+
</div>`;
|
|
4517
|
+
}).join("")}
|
|
4518
|
+
</div>
|
|
4183
4519
|
</div>`);
|
|
4184
4520
|
}
|
|
4185
|
-
if (
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
<
|
|
4521
|
+
if (indexing) {
|
|
4522
|
+
const tone = indexing.indexedPct >= 90 ? "positive" : indexing.indexedPct >= 70 ? "caution" : "negative";
|
|
4523
|
+
const fillPct = Math.max(indexing.indexedPct, 1.5);
|
|
4524
|
+
cards.push(`<div class="client-card">
|
|
4525
|
+
<h3>Pages Google can find on your site</h3>
|
|
4526
|
+
<p class="card-subtitle">Google indexing your site increases the chances of it appearing in AI search (especially Gemini).</p>
|
|
4527
|
+
<div class="client-progress-number tone-${tone}">${indexing.indexedPct}%</div>
|
|
4528
|
+
<div style="font-size:12px;color:${COLORS.textMuted};">${formatNumber(indexing.indexed)} of ${formatNumber(indexing.total)} pages indexed</div>
|
|
4529
|
+
<div class="client-progress-bar"><div class="client-progress-fill tone-${tone}" style="width:${fillPct}%"></div></div>
|
|
4530
|
+
<p style="margin:0;font-size:12px;color:${COLORS.textMuted};"><strong style="color:${COLORS.text};">${formatNumber(indexing.notIndexed)}</strong> ${pluralize(indexing.notIndexed, "page is", "pages are")} not indexed yet.</p>
|
|
4190
4531
|
</div>`);
|
|
4191
4532
|
}
|
|
4192
|
-
if (
|
|
4193
|
-
const
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4533
|
+
if (gsc) {
|
|
4534
|
+
const queries2 = gsc.topQueries.slice(0, 5);
|
|
4535
|
+
const queryRows = queries2.length > 0 ? `<div class="client-bar-list">
|
|
4536
|
+
${queries2.map((q) => {
|
|
4537
|
+
const pct = gscMax > 0 ? Math.max(q.impressions / gscMax * 100, 1.5) : 0;
|
|
4538
|
+
return `<div class="client-bar-row">
|
|
4539
|
+
<span class="bar-label">${escapeHtml(q.query)}</span>
|
|
4540
|
+
<div class="bar-track"><div class="bar-fill bar-fill-sky" style="width:${pct}%"></div></div>
|
|
4541
|
+
<span class="bar-value">${formatNumber(q.impressions)} ${pluralize(q.impressions, "search", "searches")}</span>
|
|
4542
|
+
</div>`;
|
|
4543
|
+
}).join("")}
|
|
4544
|
+
</div>` : "";
|
|
4545
|
+
cards.push(`<div class="client-card">
|
|
4546
|
+
<h3>What people search Google for</h3>
|
|
4547
|
+
<p class="card-subtitle">You appeared in <strong style="color:${COLORS.text};">${formatNumber(gsc.totalImpressions)}</strong> Google searches and got <strong style="color:${COLORS.text};">${formatNumber(gsc.totalClicks)}</strong> ${pluralize(gsc.totalClicks, "click")} this period.</p>
|
|
4548
|
+
${queryRows}
|
|
4198
4549
|
</div>`);
|
|
4199
4550
|
}
|
|
4200
|
-
const opportunities = dedupeReportOpportunities(report);
|
|
4201
4551
|
if (opportunities.length > 0) {
|
|
4202
|
-
|
|
4203
|
-
<h3>
|
|
4204
|
-
<p>
|
|
4205
|
-
<ul
|
|
4552
|
+
cards.push(`<div class="client-card">
|
|
4553
|
+
<h3>Topics where you could improve</h3>
|
|
4554
|
+
<p class="card-subtitle">Customer questions where better content on your site would help AI cite you.</p>
|
|
4555
|
+
<ul class="client-opportunity-list">
|
|
4556
|
+
${opportunities.map((o) => `<li>
|
|
4557
|
+
<div class="op-query">${escapeHtml(o.query)}</div>
|
|
4558
|
+
<div class="op-action">${escapeHtml(contentActionLabel(o.action))}</div>
|
|
4559
|
+
</li>`).join("")}
|
|
4560
|
+
</ul>
|
|
4206
4561
|
</div>`);
|
|
4207
4562
|
}
|
|
4208
4563
|
return section(
|
|
4209
4564
|
{
|
|
4210
4565
|
id: "client-evidence-summary",
|
|
4211
|
-
eyebrow: "
|
|
4212
|
-
title: "
|
|
4213
|
-
intro: "
|
|
4566
|
+
eyebrow: "What we based this on",
|
|
4567
|
+
title: "The signals behind this plan",
|
|
4568
|
+
intro: "The data behind the recommendations above. Switch to Agency for the full breakdowns."
|
|
4214
4569
|
},
|
|
4215
|
-
|
|
4570
|
+
cards.length > 0 ? `<div class="client-evidence-grid">${cards.join("")}</div>` : renderEmpty("No supporting evidence yet \u2014 this fills in after the first AI check.")
|
|
4216
4571
|
);
|
|
4217
4572
|
}
|
|
4218
4573
|
function renderAgencyDiagnostics(report) {
|
|
@@ -4242,12 +4597,12 @@ function renderReportHtml(report, opts = {}) {
|
|
|
4242
4597
|
const title = opts.title ?? `Canonry ${audience} report \u2014 ${report.meta.project.displayName}`;
|
|
4243
4598
|
const sections = audience === "client" ? [
|
|
4244
4599
|
renderClientSummary(report),
|
|
4245
|
-
renderWhatsChanged(report),
|
|
4600
|
+
renderWhatsChanged(report, "client"),
|
|
4246
4601
|
renderAudienceActionPlan(report, "client"),
|
|
4247
4602
|
renderClientEvidenceSummary(report)
|
|
4248
4603
|
].join("\n") : [
|
|
4249
4604
|
renderExecutiveSummary(report),
|
|
4250
|
-
renderWhatsChanged(report),
|
|
4605
|
+
renderWhatsChanged(report, "agency"),
|
|
4251
4606
|
renderAudienceActionPlan(report, "agency"),
|
|
4252
4607
|
renderAgencyDiagnostics(report),
|
|
4253
4608
|
renderCitationScorecard(report),
|
|
@@ -4276,12 +4631,12 @@ function renderReportHtml(report, opts = {}) {
|
|
|
4276
4631
|
<body>
|
|
4277
4632
|
<div class="container">
|
|
4278
4633
|
<header class="header">
|
|
4279
|
-
<div class="eyebrow"
|
|
4634
|
+
<div class="eyebrow">AI Visibility Report</div>
|
|
4280
4635
|
<h1>${escapeHtml(report.meta.project.displayName)}</h1>
|
|
4281
4636
|
<div class="subtitle">${escapeHtml(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml(report.meta.project.country)} / ${escapeHtml(report.meta.project.language.toUpperCase())}${renderHeaderLocationFragment(report.meta.location)} \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
|
|
4282
4637
|
</header>
|
|
4283
4638
|
${sections}
|
|
4284
|
-
<footer class="footer">Generated by canonry \xB7 ${escapeHtml(report.meta.generatedAt)}</footer>
|
|
4639
|
+
<footer class="footer">Generated by <a href="https://canonry.ai">canonry</a> \xB7 ${escapeHtml(formatIsoDate(report.meta.generatedAt))}</footer>
|
|
4285
4640
|
</div>
|
|
4286
4641
|
<script type="application/json" id="canonry-report-data">${json}</script>
|
|
4287
4642
|
</body>
|
|
@@ -10672,6 +11027,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
|
10672
11027
|
{ name: "date" },
|
|
10673
11028
|
{ name: sourceDim },
|
|
10674
11029
|
{ name: mediumDim },
|
|
11030
|
+
{ name: "sessionDefaultChannelGroup" },
|
|
10675
11031
|
{ name: "landingPagePlusQueryString" }
|
|
10676
11032
|
],
|
|
10677
11033
|
metrics: [
|
|
@@ -10697,7 +11053,8 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
|
10697
11053
|
date: row.dimensionValues[0].value,
|
|
10698
11054
|
source: row.dimensionValues[1].value,
|
|
10699
11055
|
medium: row.dimensionValues[2].value,
|
|
10700
|
-
|
|
11056
|
+
channelGroup: row.dimensionValues[3]?.value ?? "(not set)",
|
|
11057
|
+
landingPage: row.dimensionValues[4]?.value ?? "(not set)",
|
|
10701
11058
|
sessions: parseInt(row.metricValues[0].value, 10) || 0,
|
|
10702
11059
|
users: parseInt(row.metricValues[1].value, 10) || 0,
|
|
10703
11060
|
sourceDimension: dimLabel
|
|
@@ -10710,7 +11067,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
|
10710
11067
|
}
|
|
10711
11068
|
const deduped = /* @__PURE__ */ new Map();
|
|
10712
11069
|
for (const row of rows) {
|
|
10713
|
-
const key = `${row.date}::${row.source}::${row.medium}::${row.sourceDimension}::${row.landingPage}`;
|
|
11070
|
+
const key = `${row.date}::${row.source}::${row.medium}::${row.sourceDimension}::${row.channelGroup}::${row.landingPage}`;
|
|
10714
11071
|
const existing = deduped.get(key);
|
|
10715
11072
|
if (!existing) {
|
|
10716
11073
|
deduped.set(key, row);
|
|
@@ -12304,6 +12661,37 @@ function formatSharePct(numerator, total) {
|
|
|
12304
12661
|
if (rounded === 0) return "<1%";
|
|
12305
12662
|
return `${rounded}%`;
|
|
12306
12663
|
}
|
|
12664
|
+
var SOCIAL_CHANNEL_GROUPS2 = /* @__PURE__ */ new Set(["Organic Social", "Paid Social"]);
|
|
12665
|
+
function buildChannelBreakdown(input) {
|
|
12666
|
+
const aiSessions = [...input.aiSessionsByChannelGroup.values()].reduce((sum, sessions) => sum + sessions, 0);
|
|
12667
|
+
const aiOrganicOverlap = Math.min(input.organicSessions, input.aiSessionsByChannelGroup.get("Organic Search") ?? 0);
|
|
12668
|
+
const aiSocialOverlap = Math.min(
|
|
12669
|
+
input.socialSessions,
|
|
12670
|
+
[...input.aiSessionsByChannelGroup.entries()].filter(([channelGroup]) => SOCIAL_CHANNEL_GROUPS2.has(channelGroup)).reduce((sum, [, sessions]) => sum + sessions, 0)
|
|
12671
|
+
);
|
|
12672
|
+
const aiDirectOverlap = Math.min(input.directSessions, input.aiSessionsByChannelGroup.get("Direct") ?? 0);
|
|
12673
|
+
const organicSessions = Math.max(0, input.organicSessions - aiOrganicOverlap);
|
|
12674
|
+
const socialSessions = Math.max(0, input.socialSessions - aiSocialOverlap);
|
|
12675
|
+
const directSessions = Math.max(0, input.directSessions - aiDirectOverlap);
|
|
12676
|
+
const coveredSessions = organicSessions + socialSessions + directSessions + aiSessions;
|
|
12677
|
+
const otherSessions = Math.max(0, input.totalSessions - coveredSessions);
|
|
12678
|
+
const bucket = (sessions) => ({
|
|
12679
|
+
sessions,
|
|
12680
|
+
sharePct: input.totalSessions > 0 ? Math.round(sessions / input.totalSessions * 100) : 0,
|
|
12681
|
+
sharePctDisplay: formatSharePct(sessions, input.totalSessions)
|
|
12682
|
+
});
|
|
12683
|
+
return {
|
|
12684
|
+
organic: bucket(organicSessions),
|
|
12685
|
+
social: bucket(socialSessions),
|
|
12686
|
+
direct: bucket(directSessions),
|
|
12687
|
+
ai: bucket(aiSessions),
|
|
12688
|
+
other: {
|
|
12689
|
+
sessions: otherSessions,
|
|
12690
|
+
sharePct: input.totalSessions > 0 ? Math.round(otherSessions / input.totalSessions * 100) : 0,
|
|
12691
|
+
sharePctDisplay: input.totalSessions <= 0 && coveredSessions > 0 ? "\u2014" : formatSharePct(otherSessions, input.totalSessions)
|
|
12692
|
+
}
|
|
12693
|
+
};
|
|
12694
|
+
}
|
|
12307
12695
|
function pickWinningDimension(rows, tupleKey) {
|
|
12308
12696
|
const winners = /* @__PURE__ */ new Map();
|
|
12309
12697
|
for (const row of rows) {
|
|
@@ -12592,6 +12980,7 @@ async function ga4Routes(app, opts) {
|
|
|
12592
12980
|
source: row.source,
|
|
12593
12981
|
medium: row.medium,
|
|
12594
12982
|
sourceDimension: row.sourceDimension,
|
|
12983
|
+
channelGroup: row.channelGroup,
|
|
12595
12984
|
landingPage: row.landingPage,
|
|
12596
12985
|
landingPageNormalized: normalizeUrlPath(row.landingPage),
|
|
12597
12986
|
sessions: row.sessions,
|
|
@@ -12783,10 +13172,18 @@ async function ga4Routes(app, opts) {
|
|
|
12783
13172
|
GROUP BY date, source, medium
|
|
12784
13173
|
)`
|
|
12785
13174
|
).get();
|
|
12786
|
-
const
|
|
13175
|
+
const aiBySessionRows = app.db.select({
|
|
13176
|
+
channelGroup: gaAiReferrals.channelGroup,
|
|
12787
13177
|
sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
12788
13178
|
users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
12789
|
-
}).from(gaAiReferrals).where(and9(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).
|
|
13179
|
+
}).from(gaAiReferrals).where(and9(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
13180
|
+
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
13181
|
+
let aiBySessionUsers = 0;
|
|
13182
|
+
for (const row of aiBySessionRows) {
|
|
13183
|
+
aiSessionsByChannelGroup.set(row.channelGroup, row.sessions ?? 0);
|
|
13184
|
+
aiBySessionUsers += row.users ?? 0;
|
|
13185
|
+
}
|
|
13186
|
+
const aiBySessionSessions = [...aiSessionsByChannelGroup.values()].reduce((sum, sessions) => sum + sessions, 0);
|
|
12790
13187
|
const socialReferrals = app.db.select({
|
|
12791
13188
|
source: gaSocialReferrals.source,
|
|
12792
13189
|
medium: gaSocialReferrals.medium,
|
|
@@ -12801,9 +13198,18 @@ async function ga4Routes(app, opts) {
|
|
|
12801
13198
|
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
12802
13199
|
const total = summaryRow?.totalSessions ?? 0;
|
|
12803
13200
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
13201
|
+
const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
|
|
13202
|
+
const socialSessions = socialTotals?.sessions ?? 0;
|
|
13203
|
+
const channelBreakdown = buildChannelBreakdown({
|
|
13204
|
+
totalSessions: total,
|
|
13205
|
+
organicSessions: totalOrganicSessions,
|
|
13206
|
+
socialSessions,
|
|
13207
|
+
directSessions: totalDirectSessions,
|
|
13208
|
+
aiSessionsByChannelGroup
|
|
13209
|
+
});
|
|
12804
13210
|
return {
|
|
12805
13211
|
totalSessions: total,
|
|
12806
|
-
totalOrganicSessions
|
|
13212
|
+
totalOrganicSessions,
|
|
12807
13213
|
totalDirectSessions,
|
|
12808
13214
|
totalUsers: summaryRow?.totalUsers ?? 0,
|
|
12809
13215
|
topPages: rows.map((r) => ({
|
|
@@ -12830,8 +13236,8 @@ async function ga4Routes(app, opts) {
|
|
|
12830
13236
|
})),
|
|
12831
13237
|
aiSessionsDeduped: aiDeduped?.sessions ?? 0,
|
|
12832
13238
|
aiUsersDeduped: aiDeduped?.users ?? 0,
|
|
12833
|
-
aiSessionsBySession:
|
|
12834
|
-
aiUsersBySession:
|
|
13239
|
+
aiSessionsBySession: aiBySessionSessions,
|
|
13240
|
+
aiUsersBySession: aiBySessionUsers,
|
|
12835
13241
|
socialReferrals: socialReferrals.map((r) => ({
|
|
12836
13242
|
source: r.source,
|
|
12837
13243
|
medium: r.medium,
|
|
@@ -12839,18 +13245,22 @@ async function ga4Routes(app, opts) {
|
|
|
12839
13245
|
sessions: r.sessions ?? 0,
|
|
12840
13246
|
users: r.users ?? 0
|
|
12841
13247
|
})),
|
|
12842
|
-
socialSessions
|
|
13248
|
+
socialSessions,
|
|
12843
13249
|
socialUsers: socialTotals?.users ?? 0,
|
|
12844
|
-
|
|
13250
|
+
channelBreakdown,
|
|
13251
|
+
organicSharePct: total > 0 ? Math.round(totalOrganicSessions / total * 100) : 0,
|
|
12845
13252
|
aiSharePct: total > 0 ? Math.round((aiDeduped?.sessions ?? 0) / total * 100) : 0,
|
|
12846
|
-
aiSharePctBySession: total > 0 ? Math.round(
|
|
13253
|
+
aiSharePctBySession: total > 0 ? Math.round(aiBySessionSessions / total * 100) : 0,
|
|
12847
13254
|
directSharePct: total > 0 ? Math.round(totalDirectSessions / total * 100) : 0,
|
|
12848
|
-
socialSharePct: total > 0 ? Math.round(
|
|
12849
|
-
|
|
13255
|
+
socialSharePct: total > 0 ? Math.round(socialSessions / total * 100) : 0,
|
|
13256
|
+
otherSessions: channelBreakdown.other.sessions,
|
|
13257
|
+
otherSharePct: channelBreakdown.other.sharePct,
|
|
13258
|
+
otherSharePctDisplay: channelBreakdown.other.sharePctDisplay,
|
|
13259
|
+
organicSharePctDisplay: formatSharePct(totalOrganicSessions, total),
|
|
12850
13260
|
aiSharePctDisplay: formatSharePct(aiDeduped?.sessions ?? 0, total),
|
|
12851
|
-
aiSharePctBySessionDisplay: formatSharePct(
|
|
13261
|
+
aiSharePctBySessionDisplay: formatSharePct(aiBySessionSessions, total),
|
|
12852
13262
|
directSharePctDisplay: formatSharePct(totalDirectSessions, total),
|
|
12853
|
-
socialSharePctDisplay: formatSharePct(
|
|
13263
|
+
socialSharePctDisplay: formatSharePct(socialSessions, total),
|
|
12854
13264
|
lastSyncedAt: latestSync?.syncedAt ?? null,
|
|
12855
13265
|
periodStart: (() => {
|
|
12856
13266
|
const start = cutoffDate ?? summaryMeta?.periodStart ?? null;
|