@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.
@@ -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
- return `<div class="chart-card"><h3>AI engine movements</h3>
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>Engine</th><th class="numeric">Prior</th><th class="numeric">Current</th><th class="numeric">Change</th></tr></thead>
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
- <td><span class="badge tone-${tone}">${escapeHtml(reportSeverityLabel(i.severity))}</span></td>
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><tr><th>Severity</th><th>Title</th><th>Query</th><th>Provider</th></tr></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: "Section 2", title: "What's Changed", intro: w.headline },
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 wins = renderWinsLosses(w.wins, "Wins", "No new gains in the latest check.");
3451
- const regressions = renderWinsLosses(w.regressions, "Regressions", "No new regressions in the latest check.");
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: "Section 2", title: "What's Changed", intro: w.headline },
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
- if (actions.length === 0) return renderEmpty("No prioritized actions yet.");
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>Evidence details</summary>
4117
- ${why ? `<div><strong>Why</strong>${why}</div>` : ""}
4118
- ${evidence ? `<div><strong>Evidence</strong>${evidence}</div>` : ""}
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(reportHorizonLabel(action.horizon))}</span>
4126
- <span class="badge tone-neutral">${escapeHtml(reportActionCategoryLabel(action.category))}</span>
4127
- <span class="badge tone-neutral">${escapeHtml(reportConfidenceLabel(action.confidence))} confidence</span>
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>Win condition:</strong> ${escapeHtml(action.successMetric)}</div>
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" ? "Client actions" : "Agency actions",
4147
- title: audience === "client" ? "What We Recommend Next" : "Agency Action Plan",
4148
- intro: audience === "client" ? "The short list to approve and execute." : "The highest-leverage work, sorted by urgency and evidence strength."
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 metrics = `<div class="metric-grid">
4156
- <div class="metric"><div class="label">Citation coverage</div><div class="value">${s.citationRate}%</div><div class="delta">${s.citedQueryCount}/${s.totalQueryCount} tracked queries cited</div></div>
4157
- <div class="metric"><div class="label">Mention coverage</div><div class="value">${s.mentionRate}%</div><div class="delta">${s.mentionedQueryCount}/${s.totalQueryCount} tracked queries mentioned</div></div>
4158
- <div class="metric"><div class="label">Providers checked</div><div class="value">${formatNumber(s.providerCount)}</div><div class="delta">${formatNumber(s.queryCount)} tracked queries</div></div>
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 notes = report.clientSummary.confidenceNotes.length > 0 ? `<div class="client-notes">${report.clientSummary.confidenceNotes.map((note) => `<div class="client-note">${escapeHtml(note)}</div>`).join("")}</div>` : "";
4161
- return section(
4162
- {
4163
- id: "client-summary",
4164
- eyebrow: "Client summary",
4165
- title: "How You're Appearing",
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
- ${metrics}
4173
- ${notes}`
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 evidenceCards = [];
4178
- if (report.aiSourceOrigin.topDomains.length > 0) {
4179
- evidenceCards.push(`<div class="diagnostic-card tone-neutral">
4180
- <h3>Sources AI engines trust</h3>
4181
- <p>These domains appeared most often as cited sources outside your owned domain.</p>
4182
- <ul>${report.aiSourceOrigin.topDomains.slice(0, 5).map((d) => `<li>${escapeHtml(d.domain)}: ${formatNumber(d.count)} citation${d.count === 1 ? "" : "s"}</li>`).join("")}</ul>
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 (report.gsc) {
4186
- evidenceCards.push(`<div class="diagnostic-card tone-neutral">
4187
- <h3>Search demand</h3>
4188
- <p>Search Console shows ${formatNumber(report.gsc.totalImpressions)} impressions and ${formatNumber(report.gsc.totalClicks)} clicks in the report window.</p>
4189
- <ul>${report.gsc.topQueries.slice(0, 5).map((q) => `<li>${escapeHtml(q.query)}: ${formatNumber(q.impressions)} impressions</li>`).join("")}</ul>
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 (report.indexingHealth) {
4193
- const tone = report.indexingHealth.indexedPct >= 90 ? "positive" : report.indexingHealth.indexedPct >= 70 ? "caution" : "negative";
4194
- evidenceCards.push(`<div class="diagnostic-card tone-${tone}">
4195
- <h3>Indexing readiness</h3>
4196
- <p>${report.indexingHealth.indexedPct}% of inspected URLs are indexed.</p>
4197
- <ul><li>${formatNumber(report.indexingHealth.indexed)} indexed</li><li>${formatNumber(report.indexingHealth.notIndexed)} not indexed</li></ul>
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
- evidenceCards.push(`<div class="diagnostic-card tone-caution">
4203
- <h3>Content opportunities</h3>
4204
- <p>Canonry found topics where better content could improve AI citations.</p>
4205
- <ul>${opportunities.slice(0, 5).map((o) => `<li>${escapeHtml(o.query)}: ${escapeHtml(o.action)} (${Math.round(o.score)})</li>`).join("")}</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: "Evidence",
4212
- title: "Why This Is The Plan",
4213
- intro: "A concise evidence view for the client summary. The agency report keeps the full matrices and detailed tables."
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
- evidenceCards.length > 0 ? `<div class="diagnostics-grid">${evidenceCards.join("")}</div>` : renderEmpty("No supporting evidence sections are populated yet.")
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">${audience === "client" ? "AEO Client Summary" : "AEO Agency Report"}</div>
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>