@ainyc/canonry 4.12.1 → 4.13.1
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-BofSsfDl.js +302 -0
- package/assets/assets/index-D0EPNRDs.css +1 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-L4KKHRVQ.js → chunk-RIGQFQJJ.js} +434 -81
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +6 -6
- package/assets/assets/index-CCC1E6ji.js +0 -302
- package/assets/assets/index-CGXCbiM_.css +0 -1
|
@@ -2663,6 +2663,47 @@ function gscDateRange(report) {
|
|
|
2663
2663
|
function pluralize(count, singular, plural = `${singular}s`) {
|
|
2664
2664
|
return count === 1 ? singular : plural;
|
|
2665
2665
|
}
|
|
2666
|
+
var PROVIDER_DISPLAY_NAMES = {
|
|
2667
|
+
gemini: "Gemini",
|
|
2668
|
+
openai: "ChatGPT",
|
|
2669
|
+
claude: "Claude",
|
|
2670
|
+
perplexity: "Perplexity",
|
|
2671
|
+
local: "Local model",
|
|
2672
|
+
"cdp:chatgpt": "ChatGPT (browser)"
|
|
2673
|
+
};
|
|
2674
|
+
function providerDisplayName(name) {
|
|
2675
|
+
return PROVIDER_DISPLAY_NAMES[name] ?? name.charAt(0).toUpperCase() + name.slice(1);
|
|
2676
|
+
}
|
|
2677
|
+
function clientHorizonLabel(horizon) {
|
|
2678
|
+
switch (horizon) {
|
|
2679
|
+
case "immediate":
|
|
2680
|
+
return "Do now";
|
|
2681
|
+
case "short-term":
|
|
2682
|
+
return "This month";
|
|
2683
|
+
case "medium-term":
|
|
2684
|
+
return "Next quarter";
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
function clientConfidenceLabel(confidence) {
|
|
2688
|
+
switch (confidence) {
|
|
2689
|
+
case "high":
|
|
2690
|
+
return "Strong evidence";
|
|
2691
|
+
case "medium":
|
|
2692
|
+
return "Some evidence";
|
|
2693
|
+
case "low":
|
|
2694
|
+
return "Worth trying";
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
function clientTrendCopy(delta) {
|
|
2698
|
+
if (!delta) return null;
|
|
2699
|
+
if (delta.direction === "up") {
|
|
2700
|
+
return { text: `Up ${delta.deltaAbs.toFixed(1)} points since last check (was ${delta.prior}%)`, tone: "positive", arrow: "\u2191" };
|
|
2701
|
+
}
|
|
2702
|
+
if (delta.direction === "down") {
|
|
2703
|
+
return { text: `Down ${Math.abs(delta.deltaAbs).toFixed(1)} points since last check (was ${delta.prior}%)`, tone: "negative", arrow: "\u2193" };
|
|
2704
|
+
}
|
|
2705
|
+
return { text: `Holding steady since last check (was ${delta.prior}%)`, tone: "neutral", arrow: "\u2192" };
|
|
2706
|
+
}
|
|
2666
2707
|
function compactInlineList(items, limit = 3) {
|
|
2667
2708
|
const visible = items.slice(0, limit);
|
|
2668
2709
|
const more = items.length - visible.length;
|
|
@@ -3192,6 +3233,222 @@ table.report-table td .badge {
|
|
|
3192
3233
|
color: ${COLORS.textFaint};
|
|
3193
3234
|
font-size: 12px;
|
|
3194
3235
|
}
|
|
3236
|
+
.client-hero {
|
|
3237
|
+
background: ${COLORS.surface};
|
|
3238
|
+
border: 1px solid ${COLORS.border};
|
|
3239
|
+
border-radius: 16px;
|
|
3240
|
+
padding: 32px;
|
|
3241
|
+
margin-bottom: 24px;
|
|
3242
|
+
}
|
|
3243
|
+
.client-hero .client-hero-eyebrow {
|
|
3244
|
+
text-transform: uppercase;
|
|
3245
|
+
letter-spacing: 0.05em;
|
|
3246
|
+
font-size: 11px;
|
|
3247
|
+
font-weight: 600;
|
|
3248
|
+
color: ${COLORS.textFaint};
|
|
3249
|
+
}
|
|
3250
|
+
.client-hero .client-hero-number {
|
|
3251
|
+
font-size: 80px;
|
|
3252
|
+
line-height: 1;
|
|
3253
|
+
font-weight: 800;
|
|
3254
|
+
letter-spacing: -0.02em;
|
|
3255
|
+
color: ${COLORS.text};
|
|
3256
|
+
margin: 14px 0 18px;
|
|
3257
|
+
}
|
|
3258
|
+
.client-hero .client-hero-sentence {
|
|
3259
|
+
font-size: 17px;
|
|
3260
|
+
color: #d4d4d8;
|
|
3261
|
+
max-width: 720px;
|
|
3262
|
+
margin: 0;
|
|
3263
|
+
}
|
|
3264
|
+
.client-hero .client-hero-trend {
|
|
3265
|
+
margin-top: 14px;
|
|
3266
|
+
font-size: 14px;
|
|
3267
|
+
font-weight: 500;
|
|
3268
|
+
}
|
|
3269
|
+
.client-hero .client-hero-trend.tone-positive { color: ${COLORS.positive}; }
|
|
3270
|
+
.client-hero .client-hero-trend.tone-negative { color: ${COLORS.negative}; }
|
|
3271
|
+
.client-hero .client-hero-trend.tone-neutral { color: ${COLORS.textMuted}; }
|
|
3272
|
+
.client-metric-grid {
|
|
3273
|
+
display: grid;
|
|
3274
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
3275
|
+
gap: 16px;
|
|
3276
|
+
margin-bottom: 24px;
|
|
3277
|
+
}
|
|
3278
|
+
.client-metric-tile {
|
|
3279
|
+
background: ${COLORS.surface};
|
|
3280
|
+
border: 1px solid ${COLORS.border};
|
|
3281
|
+
border-radius: 12px;
|
|
3282
|
+
padding: 22px 24px;
|
|
3283
|
+
}
|
|
3284
|
+
.client-metric-tile .label {
|
|
3285
|
+
text-transform: uppercase;
|
|
3286
|
+
letter-spacing: 0.05em;
|
|
3287
|
+
font-size: 11px;
|
|
3288
|
+
font-weight: 600;
|
|
3289
|
+
color: ${COLORS.textFaint};
|
|
3290
|
+
margin-bottom: 14px;
|
|
3291
|
+
}
|
|
3292
|
+
.client-metric-tile .value {
|
|
3293
|
+
font-size: 48px;
|
|
3294
|
+
line-height: 1;
|
|
3295
|
+
font-weight: 800;
|
|
3296
|
+
letter-spacing: -0.02em;
|
|
3297
|
+
color: ${COLORS.text};
|
|
3298
|
+
}
|
|
3299
|
+
.client-metric-tile .subtitle {
|
|
3300
|
+
margin-top: 10px;
|
|
3301
|
+
font-size: 12px;
|
|
3302
|
+
color: ${COLORS.textMuted};
|
|
3303
|
+
}
|
|
3304
|
+
.client-card {
|
|
3305
|
+
background: ${COLORS.surface};
|
|
3306
|
+
border: 1px solid ${COLORS.border};
|
|
3307
|
+
border-radius: 12px;
|
|
3308
|
+
padding: 22px 24px;
|
|
3309
|
+
margin-bottom: 16px;
|
|
3310
|
+
}
|
|
3311
|
+
.client-card h3 {
|
|
3312
|
+
font-size: 15px;
|
|
3313
|
+
font-weight: 600;
|
|
3314
|
+
margin: 0 0 4px;
|
|
3315
|
+
}
|
|
3316
|
+
.client-card .card-subtitle {
|
|
3317
|
+
font-size: 12px;
|
|
3318
|
+
color: ${COLORS.textMuted};
|
|
3319
|
+
margin: 0 0 18px;
|
|
3320
|
+
}
|
|
3321
|
+
.client-bar-list {
|
|
3322
|
+
display: flex;
|
|
3323
|
+
flex-direction: column;
|
|
3324
|
+
gap: 14px;
|
|
3325
|
+
}
|
|
3326
|
+
.client-bar-row {
|
|
3327
|
+
display: grid;
|
|
3328
|
+
grid-template-columns: 140px 1fr 130px;
|
|
3329
|
+
align-items: center;
|
|
3330
|
+
gap: 14px;
|
|
3331
|
+
font-size: 13px;
|
|
3332
|
+
}
|
|
3333
|
+
.client-bar-row .bar-label { color: #d4d4d8; }
|
|
3334
|
+
.client-bar-row .bar-track {
|
|
3335
|
+
height: 10px;
|
|
3336
|
+
background: ${COLORS.border};
|
|
3337
|
+
border-radius: 999px;
|
|
3338
|
+
overflow: hidden;
|
|
3339
|
+
}
|
|
3340
|
+
.client-bar-row .bar-fill {
|
|
3341
|
+
height: 100%;
|
|
3342
|
+
border-radius: 999px;
|
|
3343
|
+
background: ${COLORS.positive}b3;
|
|
3344
|
+
}
|
|
3345
|
+
.client-bar-row .bar-fill.bar-fill-neutral { background: #a1a1aaaa; }
|
|
3346
|
+
.client-bar-row .bar-fill.bar-fill-sky { background: #38bdf8b3; }
|
|
3347
|
+
.client-bar-row .bar-value {
|
|
3348
|
+
text-align: right;
|
|
3349
|
+
font-size: 13px;
|
|
3350
|
+
font-weight: 600;
|
|
3351
|
+
color: ${COLORS.text};
|
|
3352
|
+
font-variant-numeric: tabular-nums;
|
|
3353
|
+
}
|
|
3354
|
+
.client-bar-row .bar-value-sub { color: ${COLORS.textFaint}; font-weight: 400; }
|
|
3355
|
+
.client-progress-number {
|
|
3356
|
+
font-size: 56px;
|
|
3357
|
+
font-weight: 800;
|
|
3358
|
+
line-height: 1;
|
|
3359
|
+
letter-spacing: -0.02em;
|
|
3360
|
+
margin: 12px 0 4px;
|
|
3361
|
+
}
|
|
3362
|
+
.client-progress-number.tone-positive { color: ${COLORS.positive}; }
|
|
3363
|
+
.client-progress-number.tone-caution { color: ${COLORS.caution}; }
|
|
3364
|
+
.client-progress-number.tone-negative { color: ${COLORS.negative}; }
|
|
3365
|
+
.client-progress-bar {
|
|
3366
|
+
height: 12px;
|
|
3367
|
+
background: ${COLORS.border};
|
|
3368
|
+
border-radius: 999px;
|
|
3369
|
+
overflow: hidden;
|
|
3370
|
+
margin: 12px 0 14px;
|
|
3371
|
+
}
|
|
3372
|
+
.client-progress-fill { height: 100%; border-radius: 999px; }
|
|
3373
|
+
.client-progress-fill.tone-positive { background: ${COLORS.positive}b3; }
|
|
3374
|
+
.client-progress-fill.tone-caution { background: ${COLORS.caution}b3; }
|
|
3375
|
+
.client-progress-fill.tone-negative { background: ${COLORS.negative}b3; }
|
|
3376
|
+
.client-evidence-grid {
|
|
3377
|
+
display: grid;
|
|
3378
|
+
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
|
3379
|
+
gap: 16px;
|
|
3380
|
+
}
|
|
3381
|
+
.client-opportunity-list {
|
|
3382
|
+
display: flex;
|
|
3383
|
+
flex-direction: column;
|
|
3384
|
+
gap: 8px;
|
|
3385
|
+
margin: 0;
|
|
3386
|
+
padding: 0;
|
|
3387
|
+
list-style: none;
|
|
3388
|
+
}
|
|
3389
|
+
.client-opportunity-list li {
|
|
3390
|
+
background: #09090b;
|
|
3391
|
+
border: 1px solid ${COLORS.border};
|
|
3392
|
+
border-radius: 8px;
|
|
3393
|
+
padding: 10px 14px;
|
|
3394
|
+
}
|
|
3395
|
+
.client-opportunity-list li .op-query {
|
|
3396
|
+
font-weight: 500;
|
|
3397
|
+
color: ${COLORS.text};
|
|
3398
|
+
font-size: 13px;
|
|
3399
|
+
}
|
|
3400
|
+
.client-opportunity-list li .op-action {
|
|
3401
|
+
margin-top: 2px;
|
|
3402
|
+
font-size: 11px;
|
|
3403
|
+
color: ${COLORS.textMuted};
|
|
3404
|
+
}
|
|
3405
|
+
.client-confidence-note {
|
|
3406
|
+
background: ${COLORS.surface};
|
|
3407
|
+
border: 1px solid ${COLORS.border};
|
|
3408
|
+
border-radius: 8px;
|
|
3409
|
+
padding: 10px 14px;
|
|
3410
|
+
font-size: 12px;
|
|
3411
|
+
color: ${COLORS.textMuted};
|
|
3412
|
+
margin-bottom: 6px;
|
|
3413
|
+
}
|
|
3414
|
+
.client-explainer {
|
|
3415
|
+
background: #09090b;
|
|
3416
|
+
border: 1px solid ${COLORS.border};
|
|
3417
|
+
border-radius: 12px;
|
|
3418
|
+
padding: 12px 16px;
|
|
3419
|
+
font-size: 12px;
|
|
3420
|
+
color: ${COLORS.textMuted};
|
|
3421
|
+
margin-bottom: 16px;
|
|
3422
|
+
line-height: 1.6;
|
|
3423
|
+
}
|
|
3424
|
+
.client-explainer strong { color: ${COLORS.text}; }
|
|
3425
|
+
.client-explainer .term { color: #d4d4d8; font-weight: 500; }
|
|
3426
|
+
.client-questions-list {
|
|
3427
|
+
display: grid;
|
|
3428
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
3429
|
+
gap: 8px;
|
|
3430
|
+
margin: 0;
|
|
3431
|
+
padding: 0;
|
|
3432
|
+
list-style: none;
|
|
3433
|
+
}
|
|
3434
|
+
.client-questions-list li {
|
|
3435
|
+
display: flex;
|
|
3436
|
+
align-items: flex-start;
|
|
3437
|
+
gap: 12px;
|
|
3438
|
+
background: #09090b;
|
|
3439
|
+
border: 1px solid ${COLORS.border};
|
|
3440
|
+
border-radius: 8px;
|
|
3441
|
+
padding: 10px 14px;
|
|
3442
|
+
font-size: 13px;
|
|
3443
|
+
color: #d4d4d8;
|
|
3444
|
+
}
|
|
3445
|
+
.client-questions-list li .qnum {
|
|
3446
|
+
flex-shrink: 0;
|
|
3447
|
+
font-size: 11px;
|
|
3448
|
+
font-weight: 600;
|
|
3449
|
+
color: ${COLORS.textFaint};
|
|
3450
|
+
font-variant-numeric: tabular-nums;
|
|
3451
|
+
}
|
|
3195
3452
|
@media (max-width: 760px) {
|
|
3196
3453
|
.container { padding: 32px 16px 72px; }
|
|
3197
3454
|
.executive-hero { grid-template-columns: 1fr; }
|
|
@@ -3199,6 +3456,9 @@ table.report-table td .badge {
|
|
|
3199
3456
|
.source-bar-row { grid-template-columns: 1fr; gap: 6px; }
|
|
3200
3457
|
.source-bar-value { text-align: left; }
|
|
3201
3458
|
.chart-grid { grid-template-columns: 1fr; }
|
|
3459
|
+
.client-hero .client-hero-number { font-size: 56px; }
|
|
3460
|
+
.client-metric-tile .value { font-size: 36px; }
|
|
3461
|
+
.client-bar-row { grid-template-columns: 100px 1fr 100px; gap: 10px; }
|
|
3202
3462
|
}
|
|
3203
3463
|
@media print {
|
|
3204
3464
|
body { background: white; color: black; }
|
|
@@ -3389,68 +3649,82 @@ function renderTrafficDeltaTile(label, delta, countLabel) {
|
|
|
3389
3649
|
</div>`;
|
|
3390
3650
|
}
|
|
3391
3651
|
var WHATS_CHANGED_PERIOD_DAYS = 14;
|
|
3392
|
-
function renderProviderMovements(movements) {
|
|
3652
|
+
function renderProviderMovements(movements, audience) {
|
|
3393
3653
|
const meaningful = movements.filter((m) => m.direction !== "flat");
|
|
3394
3654
|
if (meaningful.length === 0) return "";
|
|
3655
|
+
const isClient = audience === "client";
|
|
3395
3656
|
const rows = meaningful.map((m) => {
|
|
3396
3657
|
const sign = m.deltaAbs > 0 ? "+" : "";
|
|
3397
3658
|
return `<tr>
|
|
3398
|
-
<td>${escapeHtml(m.provider)}</td>
|
|
3659
|
+
<td>${escapeHtml(isClient ? providerDisplayName(m.provider) : m.provider)}</td>
|
|
3399
3660
|
<td class="numeric">${m.prior}%</td>
|
|
3400
3661
|
<td class="numeric">${m.current}%</td>
|
|
3401
3662
|
<td class="numeric ${deltaToneClass(m.direction)}">${sign}${m.deltaAbs.toFixed(1)}% ${deltaArrow(m.direction)}</td>
|
|
3402
3663
|
</tr>`;
|
|
3403
3664
|
}).join("");
|
|
3404
|
-
|
|
3665
|
+
const heading = isClient ? "How each AI tool changed" : "AI engine movements";
|
|
3666
|
+
const colA = isClient ? "AI tool" : "Engine";
|
|
3667
|
+
const colB = isClient ? "Was" : "Prior";
|
|
3668
|
+
const colC = isClient ? "Now" : "Current";
|
|
3669
|
+
return `<div class="chart-card"><h3>${heading}</h3>
|
|
3405
3670
|
<table class="report-table">
|
|
3406
|
-
<thead><tr><th
|
|
3671
|
+
<thead><tr><th>${colA}</th><th class="numeric">${colB}</th><th class="numeric">${colC}</th><th class="numeric">Change</th></tr></thead>
|
|
3407
3672
|
<tbody>${rows}</tbody>
|
|
3408
3673
|
</table>
|
|
3409
3674
|
</div>`;
|
|
3410
3675
|
}
|
|
3411
|
-
function renderWinsLosses(insights2, heading, emptyMessage) {
|
|
3676
|
+
function renderWinsLosses(insights2, heading, emptyMessage, audience) {
|
|
3412
3677
|
if (insights2.length === 0) {
|
|
3413
3678
|
return `<div class="chart-card"><h3>${escapeHtml(heading)}</h3>
|
|
3414
3679
|
<p class="section-intro">${escapeHtml(emptyMessage)}</p>
|
|
3415
3680
|
</div>`;
|
|
3416
3681
|
}
|
|
3682
|
+
const isClient = audience === "client";
|
|
3417
3683
|
const rows = insights2.map((i) => {
|
|
3418
3684
|
const tone = severityTone(i.severity);
|
|
3419
3685
|
const countChip = i.instanceCount > 1 ? ` <span class="badge tone-neutral">\xD7 ${i.instanceCount}</span>` : "";
|
|
3686
|
+
const severityCell = isClient ? "" : `<td><span class="badge tone-${tone}">${escapeHtml(reportSeverityLabel(i.severity))}</span></td>`;
|
|
3420
3687
|
return `<tr>
|
|
3421
|
-
|
|
3688
|
+
${severityCell}
|
|
3422
3689
|
<td>${escapeHtml(i.title)}${countChip}</td>
|
|
3423
3690
|
<td>${escapeHtml(i.query)}</td>
|
|
3424
|
-
<td>${escapeHtml(i.provider)}</td>
|
|
3691
|
+
<td>${escapeHtml(isClient ? providerDisplayName(i.provider) : i.provider)}</td>
|
|
3425
3692
|
</tr>`;
|
|
3426
3693
|
}).join("");
|
|
3694
|
+
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
3695
|
return `<div class="chart-card"><h3>${escapeHtml(heading)}</h3>
|
|
3428
3696
|
<table class="report-table">
|
|
3429
|
-
<thead
|
|
3697
|
+
<thead>${headers}</thead>
|
|
3430
3698
|
<tbody>${rows}</tbody>
|
|
3431
3699
|
</table>
|
|
3432
3700
|
</div>`;
|
|
3433
3701
|
}
|
|
3434
|
-
function renderWhatsChanged(report) {
|
|
3702
|
+
function renderWhatsChanged(report, audience) {
|
|
3435
3703
|
const w = report.whatsChanged;
|
|
3704
|
+
const isClient = audience === "client";
|
|
3705
|
+
const eyebrow = isClient ? "Since last check" : "Section 2";
|
|
3706
|
+
const title = isClient ? "What's different since last check" : "What's Changed";
|
|
3707
|
+
const intro = isClient ? "" : w.headline;
|
|
3436
3708
|
if (!w.enoughHistory && !w.gscClicksDelta && !w.aiReferralsDelta && w.wins.length === 0 && w.regressions.length === 0) {
|
|
3437
3709
|
return section(
|
|
3438
|
-
{ id: "whats-changed", eyebrow
|
|
3439
|
-
renderEmpty("Trends will appear after a few more checks.")
|
|
3710
|
+
{ id: "whats-changed", eyebrow, title, intro },
|
|
3711
|
+
renderEmpty(isClient ? "No comparison yet \u2014 trends will appear after a few more checks." : "Trends will appear after a few more checks.")
|
|
3440
3712
|
);
|
|
3441
3713
|
}
|
|
3442
3714
|
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")}
|
|
3715
|
+
${renderRateDeltaTile(isClient ? "AI links to your website" : "Citation rate", w.citationRate, "%")}
|
|
3716
|
+
${renderRateDeltaTile(isClient ? "AI mentions your name" : "Mention rate", w.mentionRate, "%")}
|
|
3717
|
+
${renderRateDeltaTile(isClient ? "Questions AI answered with you" : "Cited queries", w.citedQueryCount, "count")}
|
|
3718
|
+
${renderTrafficDeltaTile(isClient ? "Visitors from Google" : "GSC clicks", w.gscClicksDelta, isClient ? "visits" : "clicks")}
|
|
3719
|
+
${renderTrafficDeltaTile(isClient ? "Visitors from AI tools" : "AI referral sessions", w.aiReferralsDelta, isClient ? "visits" : "sessions")}
|
|
3448
3720
|
</div>`;
|
|
3449
|
-
const movements = renderProviderMovements(w.providerMovements);
|
|
3450
|
-
const
|
|
3451
|
-
const
|
|
3721
|
+
const movements = renderProviderMovements(w.providerMovements, audience);
|
|
3722
|
+
const winsHeading = isClient ? "What got better" : "Wins";
|
|
3723
|
+
const lossesHeading = isClient ? "What got worse" : "Regressions";
|
|
3724
|
+
const wins = renderWinsLosses(w.wins, winsHeading, isClient ? "No new wins this period." : "No new gains in the latest check.", audience);
|
|
3725
|
+
const regressions = renderWinsLosses(w.regressions, lossesHeading, isClient ? "Nothing got worse this period." : "No new regressions in the latest check.", audience);
|
|
3452
3726
|
return section(
|
|
3453
|
-
{ id: "whats-changed", eyebrow
|
|
3727
|
+
{ id: "whats-changed", eyebrow, title, intro },
|
|
3454
3728
|
`${rateTiles}${movements}${wins}${regressions}`
|
|
3455
3729
|
);
|
|
3456
3730
|
}
|
|
@@ -4104,8 +4378,9 @@ function renderRecommendedNextSteps(report) {
|
|
|
4104
4378
|
function actionAudienceMatches(action, audience) {
|
|
4105
4379
|
return action.audience === "both" || action.audience === audience;
|
|
4106
4380
|
}
|
|
4107
|
-
function renderActionCards(actions) {
|
|
4108
|
-
|
|
4381
|
+
function renderActionCards(actions, audience) {
|
|
4382
|
+
const isClient = audience === "client";
|
|
4383
|
+
if (actions.length === 0) return renderEmpty(isClient ? "No recommendations yet \u2014 run an AI check to populate this." : "No prioritized actions yet.");
|
|
4109
4384
|
return `<div class="action-card-grid">
|
|
4110
4385
|
${actions.map((action, idx) => {
|
|
4111
4386
|
const tone = reportActionTone(action);
|
|
@@ -4113,18 +4388,22 @@ function renderActionCards(actions) {
|
|
|
4113
4388
|
const evidence = action.evidence.length > 0 ? `<ul>${action.evidence.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : "";
|
|
4114
4389
|
const proof = renderProofChips(action.evidence.length > 0 ? action.evidence : action.why, 3);
|
|
4115
4390
|
const details = why || evidence ? `<details class="action-details">
|
|
4116
|
-
<summary
|
|
4117
|
-
${why ? `<div><strong
|
|
4118
|
-
${evidence ? `<div><strong
|
|
4391
|
+
<summary>${isClient ? "See the data behind this" : "Evidence details"}</summary>
|
|
4392
|
+
${why ? `<div><strong>${isClient ? "Why this matters" : "Why"}</strong>${why}</div>` : ""}
|
|
4393
|
+
${evidence ? `<div><strong>${isClient ? "What we saw" : "Evidence"}</strong>${evidence}</div>` : ""}
|
|
4119
4394
|
</details>` : "";
|
|
4395
|
+
const horizonLabel = isClient ? clientHorizonLabel(action.horizon) : reportHorizonLabel(action.horizon);
|
|
4396
|
+
const confidenceLabel = isClient ? clientConfidenceLabel(action.confidence) : `${reportConfidenceLabel(action.confidence)} confidence`;
|
|
4397
|
+
const categoryBadge = isClient ? "" : `<span class="badge tone-neutral">${escapeHtml(reportActionCategoryLabel(action.category))}</span>`;
|
|
4398
|
+
const successLabel = isClient ? "What success looks like:" : "Win condition:";
|
|
4120
4399
|
return `<article class="action-card">
|
|
4121
4400
|
<div class="action-head">
|
|
4122
|
-
<div class="action-rank" title="Impact rank \u2014 1 is the highest-leverage action">${idx + 1}</div>
|
|
4401
|
+
<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
4402
|
<div>
|
|
4124
4403
|
<div class="action-meta">
|
|
4125
|
-
<span class="badge tone-${tone}">${escapeHtml(
|
|
4126
|
-
|
|
4127
|
-
<span class="badge tone-neutral">${escapeHtml(
|
|
4404
|
+
<span class="badge tone-${tone}">${escapeHtml(horizonLabel)}</span>
|
|
4405
|
+
${categoryBadge}
|
|
4406
|
+
<span class="badge tone-neutral">${escapeHtml(confidenceLabel)}</span>
|
|
4128
4407
|
</div>
|
|
4129
4408
|
<h3>${escapeHtml(action.title)}</h3>
|
|
4130
4409
|
</div>
|
|
@@ -4132,7 +4411,7 @@ function renderActionCards(actions) {
|
|
|
4132
4411
|
<p>${escapeHtml(action.action)}</p>
|
|
4133
4412
|
${proof}
|
|
4134
4413
|
${details}
|
|
4135
|
-
<div class="success-metric"><strong
|
|
4414
|
+
<div class="success-metric"><strong>${successLabel}</strong> ${escapeHtml(action.successMetric)}</div>
|
|
4136
4415
|
</article>`;
|
|
4137
4416
|
}).join("")}
|
|
4138
4417
|
</div>`;
|
|
@@ -4143,76 +4422,150 @@ function renderAudienceActionPlan(report, audience) {
|
|
|
4143
4422
|
return section(
|
|
4144
4423
|
{
|
|
4145
4424
|
id: audience === "client" ? "client-action-plan" : "agency-action-plan",
|
|
4146
|
-
eyebrow: audience === "client" ? "
|
|
4147
|
-
title: audience === "client" ? "What
|
|
4148
|
-
intro: audience === "client" ? "
|
|
4425
|
+
eyebrow: audience === "client" ? "Action plan" : "Agency actions",
|
|
4426
|
+
title: audience === "client" ? "What to do next" : "Agency Action Plan",
|
|
4427
|
+
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
4428
|
},
|
|
4150
|
-
renderActionCards(actions)
|
|
4429
|
+
renderActionCards(actions, audience)
|
|
4151
4430
|
);
|
|
4152
4431
|
}
|
|
4153
4432
|
function renderClientSummary(report) {
|
|
4154
4433
|
const s = report.executiveSummary;
|
|
4155
|
-
const
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4434
|
+
const sc = report.citationScorecard;
|
|
4435
|
+
const totalQ = s.totalQueryCount;
|
|
4436
|
+
const heroNumber = totalQ > 0 ? `${s.citationRate}%` : "\u2014";
|
|
4437
|
+
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.";
|
|
4438
|
+
const trend = clientTrendCopy(report.whatsChanged.citationRate);
|
|
4439
|
+
const heroTrend = trend ? `<p class="client-hero-trend tone-${trend.tone}"><span style="margin-right:6px;">${trend.arrow}</span>${escapeHtml(trend.text)}</p>` : "";
|
|
4440
|
+
const hero = `<div class="client-hero">
|
|
4441
|
+
<div class="client-hero-eyebrow">Overview</div>
|
|
4442
|
+
<div class="client-hero-number">${heroNumber}</div>
|
|
4443
|
+
<p class="client-hero-sentence">${escapeHtml(heroSentence)}</p>
|
|
4444
|
+
${heroTrend}
|
|
4159
4445
|
</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>
|
|
4446
|
+
const providerSubtitle = sc.providers.length > 0 ? sc.providers.map(providerDisplayName).join(", ") : `${formatNumber(s.queryCount)} ${pluralize(s.queryCount, "question")} tested`;
|
|
4447
|
+
const tiles = `<div class="client-metric-grid">
|
|
4448
|
+
<div class="client-metric-tile">
|
|
4449
|
+
<div class="label">AI mentions your name</div>
|
|
4450
|
+
<div class="value">${s.mentionRate}%</div>
|
|
4451
|
+
<div class="subtitle">${totalQ > 0 ? `Says your name in ${s.mentionedQueryCount} of ${totalQ} ${pluralize(totalQ, "answer")}` : "No data yet"}</div>
|
|
4171
4452
|
</div>
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4453
|
+
<div class="client-metric-tile">
|
|
4454
|
+
<div class="label">AI links to your website</div>
|
|
4455
|
+
<div class="value">${s.citationRate}%</div>
|
|
4456
|
+
<div class="subtitle">${totalQ > 0 ? `Cites your site as a source in ${s.citedQueryCount} of ${totalQ} ${pluralize(totalQ, "answer")}` : "No data yet"}</div>
|
|
4457
|
+
</div>
|
|
4458
|
+
<div class="client-metric-tile">
|
|
4459
|
+
<div class="label">AI tools tested</div>
|
|
4460
|
+
<div class="value">${formatNumber(s.providerCount)}</div>
|
|
4461
|
+
<div class="subtitle">${escapeHtml(providerSubtitle)}</div>
|
|
4462
|
+
</div>
|
|
4463
|
+
</div>`;
|
|
4464
|
+
const explainer = `<div class="client-explainer">
|
|
4465
|
+
<strong>Mentions and links are different.</strong>
|
|
4466
|
+
A <span class="term">mention</span> is when AI says your name out loud in its answer.
|
|
4467
|
+
A <span class="term">link</span> is when AI lists your website as a source it used.
|
|
4468
|
+
AI can do either, both, or neither \u2014 that's why we track both.
|
|
4469
|
+
</div>`;
|
|
4470
|
+
const questions = sc.queries.length > 0 ? `<div class="client-card">
|
|
4471
|
+
<h3>Customer questions we tested</h3>
|
|
4472
|
+
<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>
|
|
4473
|
+
<ol class="client-questions-list">
|
|
4474
|
+
${sc.queries.map((q, i) => `<li><span class="qnum">${String(i + 1).padStart(2, "0")}</span><span>"${escapeHtml(q)}"</span></li>`).join("")}
|
|
4475
|
+
</ol>
|
|
4476
|
+
</div>` : "";
|
|
4477
|
+
const providerBars = sc.providerRates.length > 0 ? `<div class="client-card">
|
|
4478
|
+
<h3>How often each AI tool links to your website</h3>
|
|
4479
|
+
<p class="card-subtitle">Higher is better. Each bar shows the share of customer questions where the AI cited your site.</p>
|
|
4480
|
+
<div class="client-bar-list">
|
|
4481
|
+
${sc.providerRates.map((r) => {
|
|
4482
|
+
const pct = Math.max(r.citationRate, 1.5);
|
|
4483
|
+
return `<div class="client-bar-row">
|
|
4484
|
+
<span class="bar-label">${escapeHtml(providerDisplayName(r.provider))}</span>
|
|
4485
|
+
<div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
|
|
4486
|
+
<span class="bar-value">${r.citationRate}% <span class="bar-value-sub">(${r.citedCount}/${r.totalCount})</span></span>
|
|
4487
|
+
</div>`;
|
|
4488
|
+
}).join("")}
|
|
4489
|
+
</div>
|
|
4490
|
+
</div>` : "";
|
|
4491
|
+
const notes = report.clientSummary.confidenceNotes.length > 0 ? `<div>${report.clientSummary.confidenceNotes.map((note) => `<div class="client-confidence-note">${escapeHtml(note)}</div>`).join("")}</div>` : "";
|
|
4492
|
+
return `<section class="report-section" id="client-summary">${hero}${tiles}${explainer}${questions}${providerBars}${notes}</section>`;
|
|
4175
4493
|
}
|
|
4176
4494
|
function renderClientEvidenceSummary(report) {
|
|
4177
|
-
const
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4495
|
+
const ai = report.aiSourceOrigin.topDomains.slice(0, 5);
|
|
4496
|
+
const gsc = report.gsc;
|
|
4497
|
+
const indexing = report.indexingHealth;
|
|
4498
|
+
const opportunities = dedupeReportOpportunities(report).slice(0, 5);
|
|
4499
|
+
const aiMax = ai.length > 0 ? Math.max(...ai.map((d) => d.count)) : 0;
|
|
4500
|
+
const gscMax = gsc ? Math.max(...gsc.topQueries.slice(0, 5).map((q) => q.impressions), 1) : 0;
|
|
4501
|
+
const cards = [];
|
|
4502
|
+
if (ai.length > 0) {
|
|
4503
|
+
cards.push(`<div class="client-card">
|
|
4504
|
+
<h3>Where AI gets its answers</h3>
|
|
4505
|
+
<p class="card-subtitle">The websites AI tools cited most often when answering customer questions about your industry.</p>
|
|
4506
|
+
<div class="client-bar-list">
|
|
4507
|
+
${ai.map((d) => {
|
|
4508
|
+
const pct = aiMax > 0 ? Math.max(d.count / aiMax * 100, 1.5) : 0;
|
|
4509
|
+
const label = escapeHtml(d.domain) + (d.isCompetitor ? ' <span style="color:' + COLORS.textFaint + ';font-size:11px;">(competitor)</span>' : "");
|
|
4510
|
+
return `<div class="client-bar-row">
|
|
4511
|
+
<span class="bar-label">${label}</span>
|
|
4512
|
+
<div class="bar-track"><div class="bar-fill bar-fill-neutral" style="width:${pct}%"></div></div>
|
|
4513
|
+
<span class="bar-value">${formatNumber(d.count)}\xD7</span>
|
|
4514
|
+
</div>`;
|
|
4515
|
+
}).join("")}
|
|
4516
|
+
</div>
|
|
4183
4517
|
</div>`);
|
|
4184
4518
|
}
|
|
4185
|
-
if (
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
<
|
|
4519
|
+
if (indexing) {
|
|
4520
|
+
const tone = indexing.indexedPct >= 90 ? "positive" : indexing.indexedPct >= 70 ? "caution" : "negative";
|
|
4521
|
+
const fillPct = Math.max(indexing.indexedPct, 1.5);
|
|
4522
|
+
cards.push(`<div class="client-card">
|
|
4523
|
+
<h3>Pages Google can find on your site</h3>
|
|
4524
|
+
<p class="card-subtitle">Google indexing your site increases the chances of it appearing in AI search (especially Gemini).</p>
|
|
4525
|
+
<div class="client-progress-number tone-${tone}">${indexing.indexedPct}%</div>
|
|
4526
|
+
<div style="font-size:12px;color:${COLORS.textMuted};">${formatNumber(indexing.indexed)} of ${formatNumber(indexing.total)} pages indexed</div>
|
|
4527
|
+
<div class="client-progress-bar"><div class="client-progress-fill tone-${tone}" style="width:${fillPct}%"></div></div>
|
|
4528
|
+
<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
4529
|
</div>`);
|
|
4191
4530
|
}
|
|
4192
|
-
if (
|
|
4193
|
-
const
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4531
|
+
if (gsc) {
|
|
4532
|
+
const queries2 = gsc.topQueries.slice(0, 5);
|
|
4533
|
+
const queryRows = queries2.length > 0 ? `<div class="client-bar-list">
|
|
4534
|
+
${queries2.map((q) => {
|
|
4535
|
+
const pct = gscMax > 0 ? Math.max(q.impressions / gscMax * 100, 1.5) : 0;
|
|
4536
|
+
return `<div class="client-bar-row">
|
|
4537
|
+
<span class="bar-label">${escapeHtml(q.query)}</span>
|
|
4538
|
+
<div class="bar-track"><div class="bar-fill bar-fill-sky" style="width:${pct}%"></div></div>
|
|
4539
|
+
<span class="bar-value">${formatNumber(q.impressions)} ${pluralize(q.impressions, "search", "searches")}</span>
|
|
4540
|
+
</div>`;
|
|
4541
|
+
}).join("")}
|
|
4542
|
+
</div>` : "";
|
|
4543
|
+
cards.push(`<div class="client-card">
|
|
4544
|
+
<h3>What people search Google for</h3>
|
|
4545
|
+
<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>
|
|
4546
|
+
${queryRows}
|
|
4198
4547
|
</div>`);
|
|
4199
4548
|
}
|
|
4200
|
-
const opportunities = dedupeReportOpportunities(report);
|
|
4201
4549
|
if (opportunities.length > 0) {
|
|
4202
|
-
|
|
4203
|
-
<h3>
|
|
4204
|
-
<p>
|
|
4205
|
-
<ul
|
|
4550
|
+
cards.push(`<div class="client-card">
|
|
4551
|
+
<h3>Topics where you could improve</h3>
|
|
4552
|
+
<p class="card-subtitle">Customer questions where better content on your site would help AI cite you.</p>
|
|
4553
|
+
<ul class="client-opportunity-list">
|
|
4554
|
+
${opportunities.map((o) => `<li>
|
|
4555
|
+
<div class="op-query">${escapeHtml(o.query)}</div>
|
|
4556
|
+
<div class="op-action">${escapeHtml(contentActionLabel(o.action))}</div>
|
|
4557
|
+
</li>`).join("")}
|
|
4558
|
+
</ul>
|
|
4206
4559
|
</div>`);
|
|
4207
4560
|
}
|
|
4208
4561
|
return section(
|
|
4209
4562
|
{
|
|
4210
4563
|
id: "client-evidence-summary",
|
|
4211
|
-
eyebrow: "
|
|
4212
|
-
title: "
|
|
4213
|
-
intro: "
|
|
4564
|
+
eyebrow: "What we based this on",
|
|
4565
|
+
title: "The signals behind this plan",
|
|
4566
|
+
intro: "The data behind the recommendations above. Switch to Agency for the full breakdowns."
|
|
4214
4567
|
},
|
|
4215
|
-
|
|
4568
|
+
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
4569
|
);
|
|
4217
4570
|
}
|
|
4218
4571
|
function renderAgencyDiagnostics(report) {
|
|
@@ -4242,12 +4595,12 @@ function renderReportHtml(report, opts = {}) {
|
|
|
4242
4595
|
const title = opts.title ?? `Canonry ${audience} report \u2014 ${report.meta.project.displayName}`;
|
|
4243
4596
|
const sections = audience === "client" ? [
|
|
4244
4597
|
renderClientSummary(report),
|
|
4245
|
-
renderWhatsChanged(report),
|
|
4598
|
+
renderWhatsChanged(report, "client"),
|
|
4246
4599
|
renderAudienceActionPlan(report, "client"),
|
|
4247
4600
|
renderClientEvidenceSummary(report)
|
|
4248
4601
|
].join("\n") : [
|
|
4249
4602
|
renderExecutiveSummary(report),
|
|
4250
|
-
renderWhatsChanged(report),
|
|
4603
|
+
renderWhatsChanged(report, "agency"),
|
|
4251
4604
|
renderAudienceActionPlan(report, "agency"),
|
|
4252
4605
|
renderAgencyDiagnostics(report),
|
|
4253
4606
|
renderCitationScorecard(report),
|
|
@@ -4276,7 +4629,7 @@ function renderReportHtml(report, opts = {}) {
|
|
|
4276
4629
|
<body>
|
|
4277
4630
|
<div class="container">
|
|
4278
4631
|
<header class="header">
|
|
4279
|
-
<div class="eyebrow"
|
|
4632
|
+
<div class="eyebrow">AI Visibility Report</div>
|
|
4280
4633
|
<h1>${escapeHtml(report.meta.project.displayName)}</h1>
|
|
4281
4634
|
<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
4635
|
</header>
|