@ainyc/canonry 4.7.2 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/assets/{index-Ca3kZYGw.js → index-C2ZxtVjD.js} +68 -68
- package/assets/index.html +1 -1
- package/dist/{chunk-VDEMEI64.js → chunk-5KIFQH52.js} +1 -1
- package/dist/{chunk-XAW66QUX.js → chunk-IJEP6LB4.js} +78 -0
- package/dist/{chunk-DVTPGC6O.js → chunk-O4VXWABZ.js} +632 -178
- package/dist/{chunk-OOADR2Q5.js → chunk-QEPFB7UW.js} +48 -2
- package/dist/cli.js +5 -5
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-ABHO5HHA.js → intelligence-service-X7CBN7S4.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +9 -9
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
configExists,
|
|
5
5
|
loadConfig,
|
|
6
6
|
saveConfigPatch
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-5KIFQH52.js";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
10
10
|
IntelligenceService,
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
runs,
|
|
62
62
|
schedules,
|
|
63
63
|
usageCounters
|
|
64
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-QEPFB7UW.js";
|
|
65
65
|
import {
|
|
66
66
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
67
67
|
AGENT_PROVIDER_IDS,
|
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
RunStatuses,
|
|
78
78
|
RunTriggers,
|
|
79
79
|
absolutizeProjectUrl,
|
|
80
|
+
actionConfidenceLabel,
|
|
80
81
|
agentBusy,
|
|
81
82
|
agentMemoryDeleteRequestSchema,
|
|
82
83
|
agentMemoryUpsertRequestSchema,
|
|
@@ -89,6 +90,7 @@ import {
|
|
|
89
90
|
categoryLabel,
|
|
90
91
|
citationStateToCited,
|
|
91
92
|
competitorBatchRequestSchema,
|
|
93
|
+
contentActionLabel,
|
|
92
94
|
deliveryFailed,
|
|
93
95
|
determineAnswerMentioned,
|
|
94
96
|
effectiveDomains,
|
|
@@ -114,7 +116,11 @@ import {
|
|
|
114
116
|
providerError,
|
|
115
117
|
queryGenerateRequestSchema,
|
|
116
118
|
registrableDomain,
|
|
119
|
+
reportActionCategoryLabel,
|
|
117
120
|
reportActionTone,
|
|
121
|
+
reportConfidenceLabel,
|
|
122
|
+
reportHorizonLabel,
|
|
123
|
+
reportSeverityLabel,
|
|
118
124
|
resolveConfigSpecQueries,
|
|
119
125
|
resolveSnapshotRequestQueries,
|
|
120
126
|
runInProgress,
|
|
@@ -129,7 +135,7 @@ import {
|
|
|
129
135
|
visibilityStateFromAnswerMentioned,
|
|
130
136
|
windowCutoff,
|
|
131
137
|
wordpressEnvSchema
|
|
132
|
-
} from "./chunk-
|
|
138
|
+
} from "./chunk-IJEP6LB4.js";
|
|
133
139
|
|
|
134
140
|
// src/telemetry.ts
|
|
135
141
|
import crypto from "crypto";
|
|
@@ -2622,12 +2628,43 @@ function formatLandingPageHtml(raw) {
|
|
|
2622
2628
|
function formatDate(iso) {
|
|
2623
2629
|
if (!iso) return "\u2014";
|
|
2624
2630
|
try {
|
|
2625
|
-
const
|
|
2626
|
-
|
|
2631
|
+
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
2632
|
+
const options = { month: "short", day: "numeric", year: "numeric" };
|
|
2633
|
+
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);
|
|
2634
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
2635
|
+
return d.toLocaleDateString("en-US", dateOnly ? { ...options, timeZone: "UTC" } : options);
|
|
2627
2636
|
} catch {
|
|
2628
2637
|
return iso;
|
|
2629
2638
|
}
|
|
2630
2639
|
}
|
|
2640
|
+
function formatDateRange(start, end) {
|
|
2641
|
+
if (!start && !end) return "";
|
|
2642
|
+
if (start && end) return `${formatDate(start)} \u2192 ${formatDate(end)}`;
|
|
2643
|
+
return formatDate(start || end);
|
|
2644
|
+
}
|
|
2645
|
+
function gscDateRange(report) {
|
|
2646
|
+
const summary = report.executiveSummary.gsc;
|
|
2647
|
+
const gsc = report.gsc;
|
|
2648
|
+
const start = summary?.periodStart || gsc?.periodStart || gsc?.trend[0]?.date || "";
|
|
2649
|
+
const end = summary?.periodEnd || gsc?.periodEnd || gsc?.trend.at(-1)?.date || "";
|
|
2650
|
+
return formatDateRange(start, end);
|
|
2651
|
+
}
|
|
2652
|
+
function pluralize(count, singular, plural = `${singular}s`) {
|
|
2653
|
+
return count === 1 ? singular : plural;
|
|
2654
|
+
}
|
|
2655
|
+
function compactInlineList(items, limit = 3) {
|
|
2656
|
+
const visible = items.slice(0, limit);
|
|
2657
|
+
const more = items.length - visible.length;
|
|
2658
|
+
return `${visible.join(", ")}${more > 0 ? `, +${more} more` : ""}`;
|
|
2659
|
+
}
|
|
2660
|
+
function renderProofChips(items, limit = 3) {
|
|
2661
|
+
if (items.length === 0) return "";
|
|
2662
|
+
const visible = items.slice(0, limit);
|
|
2663
|
+
const more = items.length - visible.length;
|
|
2664
|
+
const chips = visible.map((item) => `<span class="proof-chip">${escapeHtml(item)}</span>`);
|
|
2665
|
+
if (more > 0) chips.push(`<span class="proof-chip">+${more} more</span>`);
|
|
2666
|
+
return `<div class="proof-chips">${chips.join("")}</div>`;
|
|
2667
|
+
}
|
|
2631
2668
|
function pressureTone(label) {
|
|
2632
2669
|
if (label === "High") return "negative";
|
|
2633
2670
|
if (label === "Moderate") return "caution";
|
|
@@ -2674,7 +2711,7 @@ body {
|
|
|
2674
2711
|
font-size: 32px;
|
|
2675
2712
|
font-weight: 700;
|
|
2676
2713
|
margin: 0 0 8px;
|
|
2677
|
-
letter-spacing:
|
|
2714
|
+
letter-spacing: 0;
|
|
2678
2715
|
}
|
|
2679
2716
|
.header .subtitle {
|
|
2680
2717
|
color: ${COLORS.textMuted};
|
|
@@ -2682,7 +2719,7 @@ body {
|
|
|
2682
2719
|
}
|
|
2683
2720
|
.eyebrow {
|
|
2684
2721
|
text-transform: uppercase;
|
|
2685
|
-
letter-spacing: 0
|
|
2722
|
+
letter-spacing: 0;
|
|
2686
2723
|
font-size: 10px;
|
|
2687
2724
|
color: ${COLORS.textFaint};
|
|
2688
2725
|
font-weight: 600;
|
|
@@ -2695,11 +2732,75 @@ section.report-section h2 {
|
|
|
2695
2732
|
font-size: 22px;
|
|
2696
2733
|
font-weight: 700;
|
|
2697
2734
|
margin: 0 0 24px;
|
|
2698
|
-
letter-spacing:
|
|
2735
|
+
letter-spacing: 0;
|
|
2699
2736
|
}
|
|
2700
2737
|
section.report-section .section-intro {
|
|
2701
2738
|
color: ${COLORS.textMuted};
|
|
2702
2739
|
margin-bottom: 24px;
|
|
2740
|
+
max-width: 760px;
|
|
2741
|
+
}
|
|
2742
|
+
.executive-hero {
|
|
2743
|
+
display: grid;
|
|
2744
|
+
grid-template-columns: minmax(0, 1.35fr) minmax(240px, 0.65fr);
|
|
2745
|
+
gap: 16px;
|
|
2746
|
+
margin-bottom: 16px;
|
|
2747
|
+
}
|
|
2748
|
+
.headline-card {
|
|
2749
|
+
background: #111827;
|
|
2750
|
+
border: 1px solid ${COLORS.border};
|
|
2751
|
+
border-radius: 8px;
|
|
2752
|
+
padding: 28px;
|
|
2753
|
+
min-height: 220px;
|
|
2754
|
+
display: flex;
|
|
2755
|
+
flex-direction: column;
|
|
2756
|
+
justify-content: space-between;
|
|
2757
|
+
}
|
|
2758
|
+
.headline-card .hero-kicker {
|
|
2759
|
+
color: ${COLORS.textMuted};
|
|
2760
|
+
font-size: 12px;
|
|
2761
|
+
font-weight: 600;
|
|
2762
|
+
text-transform: uppercase;
|
|
2763
|
+
letter-spacing: 0;
|
|
2764
|
+
}
|
|
2765
|
+
.headline-card .hero-title {
|
|
2766
|
+
font-size: 44px;
|
|
2767
|
+
line-height: 1.05;
|
|
2768
|
+
font-weight: 800;
|
|
2769
|
+
letter-spacing: 0;
|
|
2770
|
+
margin: 18px 0;
|
|
2771
|
+
}
|
|
2772
|
+
.headline-card .hero-subtitle {
|
|
2773
|
+
color: ${COLORS.textMuted};
|
|
2774
|
+
font-size: 15px;
|
|
2775
|
+
max-width: 620px;
|
|
2776
|
+
}
|
|
2777
|
+
.hero-proof-grid {
|
|
2778
|
+
display: grid;
|
|
2779
|
+
gap: 12px;
|
|
2780
|
+
}
|
|
2781
|
+
.hero-proof {
|
|
2782
|
+
background: ${COLORS.surface};
|
|
2783
|
+
border: 1px solid ${COLORS.border};
|
|
2784
|
+
border-radius: 8px;
|
|
2785
|
+
padding: 18px;
|
|
2786
|
+
}
|
|
2787
|
+
.hero-proof .mini-label {
|
|
2788
|
+
color: ${COLORS.textFaint};
|
|
2789
|
+
font-size: 10px;
|
|
2790
|
+
font-weight: 600;
|
|
2791
|
+
text-transform: uppercase;
|
|
2792
|
+
letter-spacing: 0;
|
|
2793
|
+
margin-bottom: 8px;
|
|
2794
|
+
}
|
|
2795
|
+
.hero-proof .mini-value {
|
|
2796
|
+
font-size: 30px;
|
|
2797
|
+
line-height: 1;
|
|
2798
|
+
font-weight: 800;
|
|
2799
|
+
}
|
|
2800
|
+
.hero-proof .mini-copy {
|
|
2801
|
+
color: ${COLORS.textMuted};
|
|
2802
|
+
font-size: 12px;
|
|
2803
|
+
margin-top: 8px;
|
|
2703
2804
|
}
|
|
2704
2805
|
.metric-grid {
|
|
2705
2806
|
display: grid;
|
|
@@ -2714,7 +2815,7 @@ section.report-section .section-intro {
|
|
|
2714
2815
|
}
|
|
2715
2816
|
.metric .label {
|
|
2716
2817
|
text-transform: uppercase;
|
|
2717
|
-
letter-spacing: 0
|
|
2818
|
+
letter-spacing: 0;
|
|
2718
2819
|
font-size: 10px;
|
|
2719
2820
|
color: ${COLORS.textFaint};
|
|
2720
2821
|
font-weight: 600;
|
|
@@ -2723,7 +2824,7 @@ section.report-section .section-intro {
|
|
|
2723
2824
|
.metric .value {
|
|
2724
2825
|
font-size: 28px;
|
|
2725
2826
|
font-weight: 700;
|
|
2726
|
-
letter-spacing:
|
|
2827
|
+
letter-spacing: 0;
|
|
2727
2828
|
}
|
|
2728
2829
|
.metric .delta {
|
|
2729
2830
|
font-size: 12px;
|
|
@@ -2748,10 +2849,46 @@ section.report-section .section-intro {
|
|
|
2748
2849
|
.finding.tone-neutral { border-left-color: ${COLORS.neutral}; }
|
|
2749
2850
|
.finding strong { display: block; margin-bottom: 4px; }
|
|
2750
2851
|
.finding span { color: ${COLORS.textMuted}; font-size: 13px; }
|
|
2751
|
-
.
|
|
2752
|
-
.
|
|
2753
|
-
|
|
2754
|
-
|
|
2852
|
+
.market-scope-card { margin-top: 16px; }
|
|
2853
|
+
.market-scope-grid {
|
|
2854
|
+
display: grid;
|
|
2855
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
2856
|
+
gap: 12px;
|
|
2857
|
+
}
|
|
2858
|
+
.scope-tile {
|
|
2859
|
+
background: #09090b;
|
|
2860
|
+
border: 1px solid ${COLORS.border};
|
|
2861
|
+
border-radius: 8px;
|
|
2862
|
+
padding: 14px;
|
|
2863
|
+
}
|
|
2864
|
+
.scope-tile .scope-label {
|
|
2865
|
+
color: ${COLORS.textFaint};
|
|
2866
|
+
font-size: 10px;
|
|
2867
|
+
font-weight: 600;
|
|
2868
|
+
text-transform: uppercase;
|
|
2869
|
+
letter-spacing: 0;
|
|
2870
|
+
margin-bottom: 8px;
|
|
2871
|
+
}
|
|
2872
|
+
.scope-tile .scope-value {
|
|
2873
|
+
font-size: 18px;
|
|
2874
|
+
line-height: 1.2;
|
|
2875
|
+
font-weight: 700;
|
|
2876
|
+
}
|
|
2877
|
+
.scope-tile .scope-copy {
|
|
2878
|
+
color: ${COLORS.textMuted};
|
|
2879
|
+
font-size: 12px;
|
|
2880
|
+
margin-top: 8px;
|
|
2881
|
+
}
|
|
2882
|
+
.scope-warning {
|
|
2883
|
+
margin-top: 12px;
|
|
2884
|
+
border: 1px solid ${COLORS.caution}55;
|
|
2885
|
+
background: ${COLORS.caution}14;
|
|
2886
|
+
border-radius: 8px;
|
|
2887
|
+
padding: 12px 14px;
|
|
2888
|
+
color: ${COLORS.textMuted};
|
|
2889
|
+
font-size: 13px;
|
|
2890
|
+
}
|
|
2891
|
+
.scope-warning strong { color: ${COLORS.text}; display: block; margin-bottom: 4px; }
|
|
2755
2892
|
.source-origin-headline { margin: 0 0 12px; font-size: 14px; color: ${COLORS.text}; }
|
|
2756
2893
|
.source-origin-headline strong { color: ${COLORS.text}; }
|
|
2757
2894
|
.source-bars { display: flex; flex-direction: column; gap: 6px; }
|
|
@@ -2773,18 +2910,24 @@ table.report-table th, table.report-table td {
|
|
|
2773
2910
|
padding: 10px 12px;
|
|
2774
2911
|
border-bottom: 1px solid ${COLORS.border};
|
|
2775
2912
|
vertical-align: top;
|
|
2776
|
-
overflow-wrap:
|
|
2777
|
-
|
|
2913
|
+
overflow-wrap: break-word;
|
|
2914
|
+
hyphens: auto;
|
|
2778
2915
|
}
|
|
2779
2916
|
table.report-table th {
|
|
2780
2917
|
font-weight: 600;
|
|
2781
2918
|
color: ${COLORS.textMuted};
|
|
2782
2919
|
text-transform: uppercase;
|
|
2783
|
-
letter-spacing: 0
|
|
2920
|
+
letter-spacing: 0;
|
|
2784
2921
|
font-size: 10px;
|
|
2785
2922
|
}
|
|
2786
2923
|
table.report-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
2787
2924
|
table.report-table td.page-cell { max-width: 0; }
|
|
2925
|
+
table.insights-table { table-layout: fixed; }
|
|
2926
|
+
table.insights-table th.col-severity, table.insights-table td.col-severity { width: 96px; }
|
|
2927
|
+
table.insights-table th.col-query, table.insights-table td.col-query { width: 18%; }
|
|
2928
|
+
table.insights-table th.col-provider, table.insights-table td.col-provider { width: 88px; }
|
|
2929
|
+
table.insights-table th.col-title, table.insights-table td.col-title { width: 28%; }
|
|
2930
|
+
table.insights-table th.col-recommendation, table.insights-table td.col-recommendation { width: auto; }
|
|
2788
2931
|
table.report-table td.page-cell .page-path {
|
|
2789
2932
|
display: block;
|
|
2790
2933
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
@@ -2877,7 +3020,7 @@ table.report-table td .badge {
|
|
|
2877
3020
|
.step .horizon {
|
|
2878
3021
|
text-transform: uppercase;
|
|
2879
3022
|
font-size: 10px;
|
|
2880
|
-
letter-spacing: 0
|
|
3023
|
+
letter-spacing: 0;
|
|
2881
3024
|
color: ${COLORS.textFaint};
|
|
2882
3025
|
font-weight: 600;
|
|
2883
3026
|
}
|
|
@@ -2892,20 +3035,40 @@ table.report-table td .badge {
|
|
|
2892
3035
|
background: ${COLORS.surface};
|
|
2893
3036
|
border: 1px solid ${COLORS.border};
|
|
2894
3037
|
border-radius: 8px;
|
|
2895
|
-
padding: 18px
|
|
3038
|
+
padding: 18px;
|
|
3039
|
+
display: flex;
|
|
3040
|
+
flex-direction: column;
|
|
3041
|
+
gap: 12px;
|
|
3042
|
+
}
|
|
3043
|
+
.action-card .action-head {
|
|
3044
|
+
display: grid;
|
|
3045
|
+
grid-template-columns: 42px 1fr;
|
|
3046
|
+
gap: 12px;
|
|
3047
|
+
align-items: start;
|
|
3048
|
+
}
|
|
3049
|
+
.action-card .action-rank {
|
|
3050
|
+
border: 1px solid ${COLORS.border};
|
|
3051
|
+
border-radius: 8px;
|
|
3052
|
+
height: 42px;
|
|
3053
|
+
display: flex;
|
|
3054
|
+
align-items: center;
|
|
3055
|
+
justify-content: center;
|
|
3056
|
+
font-size: 16px;
|
|
3057
|
+
font-weight: 800;
|
|
3058
|
+
color: ${COLORS.text};
|
|
3059
|
+
background: #09090b;
|
|
2896
3060
|
}
|
|
2897
3061
|
.action-card .action-meta {
|
|
2898
3062
|
display: flex;
|
|
2899
3063
|
flex-wrap: wrap;
|
|
2900
3064
|
gap: 8px;
|
|
2901
|
-
margin-bottom: 10px;
|
|
2902
3065
|
}
|
|
2903
3066
|
.action-card h3 {
|
|
2904
3067
|
font-size: 16px;
|
|
2905
|
-
margin: 0 0
|
|
3068
|
+
margin: 8px 0 0;
|
|
2906
3069
|
}
|
|
2907
3070
|
.action-card p {
|
|
2908
|
-
margin: 0
|
|
3071
|
+
margin: 0;
|
|
2909
3072
|
color: ${COLORS.textMuted};
|
|
2910
3073
|
}
|
|
2911
3074
|
.action-card ul {
|
|
@@ -2915,6 +3078,28 @@ table.report-table td .badge {
|
|
|
2915
3078
|
font-size: 13px;
|
|
2916
3079
|
}
|
|
2917
3080
|
.action-card li { margin: 4px 0; }
|
|
3081
|
+
.proof-chips {
|
|
3082
|
+
display: flex;
|
|
3083
|
+
flex-wrap: wrap;
|
|
3084
|
+
gap: 8px;
|
|
3085
|
+
}
|
|
3086
|
+
.proof-chip {
|
|
3087
|
+
border: 1px solid ${COLORS.border};
|
|
3088
|
+
border-radius: 8px;
|
|
3089
|
+
padding: 6px 8px;
|
|
3090
|
+
color: ${COLORS.textMuted};
|
|
3091
|
+
font-size: 12px;
|
|
3092
|
+
background: #09090b;
|
|
3093
|
+
}
|
|
3094
|
+
.action-details {
|
|
3095
|
+
color: ${COLORS.textMuted};
|
|
3096
|
+
font-size: 12px;
|
|
3097
|
+
}
|
|
3098
|
+
.action-details summary {
|
|
3099
|
+
cursor: pointer;
|
|
3100
|
+
color: ${COLORS.text};
|
|
3101
|
+
font-weight: 600;
|
|
3102
|
+
}
|
|
2918
3103
|
.action-card .success-metric {
|
|
2919
3104
|
color: ${COLORS.text};
|
|
2920
3105
|
font-size: 13px;
|
|
@@ -2950,10 +3135,44 @@ table.report-table td .badge {
|
|
|
2950
3135
|
.diagnostic-card h3 { font-size: 14px; margin: 0 0 6px; }
|
|
2951
3136
|
.diagnostic-card p { margin: 0 0 8px; color: ${COLORS.textMuted}; font-size: 13px; }
|
|
2952
3137
|
.diagnostic-card ul { margin: 0; padding-left: 16px; color: ${COLORS.textMuted}; font-size: 12px; }
|
|
3138
|
+
.diagnostic-card .proof-chips { margin-top: 10px; }
|
|
2953
3139
|
.diagnostic-card.tone-positive { border-left-color: ${COLORS.positive}; }
|
|
2954
3140
|
.diagnostic-card.tone-caution { border-left-color: ${COLORS.caution}; }
|
|
2955
3141
|
.diagnostic-card.tone-negative { border-left-color: ${COLORS.negative}; }
|
|
2956
3142
|
.diagnostic-card.tone-neutral { border-left-color: ${COLORS.neutral}; }
|
|
3143
|
+
.opportunity-grid {
|
|
3144
|
+
display: grid;
|
|
3145
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
3146
|
+
gap: 12px;
|
|
3147
|
+
margin-bottom: 16px;
|
|
3148
|
+
}
|
|
3149
|
+
.opportunity-card {
|
|
3150
|
+
background: ${COLORS.surface};
|
|
3151
|
+
border: 1px solid ${COLORS.border};
|
|
3152
|
+
border-radius: 8px;
|
|
3153
|
+
padding: 16px;
|
|
3154
|
+
}
|
|
3155
|
+
.opportunity-card .opportunity-score {
|
|
3156
|
+
font-size: 32px;
|
|
3157
|
+
line-height: 1;
|
|
3158
|
+
font-weight: 800;
|
|
3159
|
+
margin-bottom: 10px;
|
|
3160
|
+
}
|
|
3161
|
+
.opportunity-card .opportunity-score-suffix {
|
|
3162
|
+
font-size: 14px;
|
|
3163
|
+
font-weight: 600;
|
|
3164
|
+
color: ${COLORS.textFaint};
|
|
3165
|
+
margin-left: 4px;
|
|
3166
|
+
}
|
|
3167
|
+
.opportunity-card h3 {
|
|
3168
|
+
font-size: 14px;
|
|
3169
|
+
margin: 0 0 8px;
|
|
3170
|
+
}
|
|
3171
|
+
.opportunity-card p {
|
|
3172
|
+
color: ${COLORS.textMuted};
|
|
3173
|
+
font-size: 12px;
|
|
3174
|
+
margin: 0;
|
|
3175
|
+
}
|
|
2957
3176
|
.footer {
|
|
2958
3177
|
margin-top: 96px;
|
|
2959
3178
|
padding-top: 24px;
|
|
@@ -2962,6 +3181,14 @@ table.report-table td .badge {
|
|
|
2962
3181
|
color: ${COLORS.textFaint};
|
|
2963
3182
|
font-size: 12px;
|
|
2964
3183
|
}
|
|
3184
|
+
@media (max-width: 760px) {
|
|
3185
|
+
.container { padding: 32px 16px 72px; }
|
|
3186
|
+
.executive-hero { grid-template-columns: 1fr; }
|
|
3187
|
+
.headline-card .hero-title { font-size: 34px; }
|
|
3188
|
+
.source-bar-row { grid-template-columns: 1fr; gap: 6px; }
|
|
3189
|
+
.source-bar-value { text-align: left; }
|
|
3190
|
+
.chart-grid { grid-template-columns: 1fr; }
|
|
3191
|
+
}
|
|
2965
3192
|
@media print {
|
|
2966
3193
|
body { background: white; color: black; }
|
|
2967
3194
|
section.report-section { break-inside: avoid; }
|
|
@@ -2984,43 +3211,108 @@ function locationDisplay(location) {
|
|
|
2984
3211
|
return place ? `${location.label} (${place})` : location.label;
|
|
2985
3212
|
}
|
|
2986
3213
|
function renderHeaderLocationFragment(location) {
|
|
2987
|
-
if (!location) return " \xB7 No
|
|
2988
|
-
return ` \xB7
|
|
3214
|
+
if (!location) return " \xB7 No market set";
|
|
3215
|
+
return ` \xB7 Market: ${escapeHtml(locationDisplay(location))}`;
|
|
3216
|
+
}
|
|
3217
|
+
var REPORT_INTENT_STOPWORDS = /* @__PURE__ */ new Set([
|
|
3218
|
+
"a",
|
|
3219
|
+
"an",
|
|
3220
|
+
"and",
|
|
3221
|
+
"for",
|
|
3222
|
+
"from",
|
|
3223
|
+
"in",
|
|
3224
|
+
"near",
|
|
3225
|
+
"of",
|
|
3226
|
+
"on",
|
|
3227
|
+
"or",
|
|
3228
|
+
"the",
|
|
3229
|
+
"to"
|
|
3230
|
+
]);
|
|
3231
|
+
function reportIntentModifiers(report) {
|
|
3232
|
+
const location = report.meta.location;
|
|
3233
|
+
if (!location) return /* @__PURE__ */ new Set();
|
|
3234
|
+
return new Set(
|
|
3235
|
+
[location.label, location.city, location.region, location.country].flatMap(tokenizeReportIntent).map(normalizeReportIntentToken).filter(Boolean)
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
function dedupeReportActions(report, actions) {
|
|
3239
|
+
const modifiers = reportIntentModifiers(report);
|
|
3240
|
+
if (actions.length <= 1 || modifiers.size === 0) return [...actions];
|
|
3241
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3242
|
+
const result = [];
|
|
3243
|
+
for (const action of actions) {
|
|
3244
|
+
if (action.category !== "content") {
|
|
3245
|
+
result.push(action);
|
|
3246
|
+
continue;
|
|
3247
|
+
}
|
|
3248
|
+
const key = reportIntentKey(extractActionQuery(action), modifiers);
|
|
3249
|
+
if (!key || seen.has(key)) continue;
|
|
3250
|
+
seen.add(key);
|
|
3251
|
+
result.push(action);
|
|
3252
|
+
}
|
|
3253
|
+
return result;
|
|
3254
|
+
}
|
|
3255
|
+
function dedupeReportOpportunities(report) {
|
|
3256
|
+
const modifiers = reportIntentModifiers(report);
|
|
3257
|
+
const opportunities = report.contentOpportunities;
|
|
3258
|
+
if (opportunities.length <= 1 || modifiers.size === 0) return opportunities;
|
|
3259
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3260
|
+
return opportunities.filter((opportunity) => {
|
|
3261
|
+
const key = reportIntentKey(opportunity.query, modifiers);
|
|
3262
|
+
if (!key || seen.has(key)) return false;
|
|
3263
|
+
seen.add(key);
|
|
3264
|
+
return true;
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
function extractActionQuery(action) {
|
|
3268
|
+
return action.title.match(/"([^"]+)"/)?.[1] ?? action.successMetric.match(/"([^"]+)"/)?.[1] ?? action.title;
|
|
3269
|
+
}
|
|
3270
|
+
function reportIntentKey(value, modifiers) {
|
|
3271
|
+
const tokens = tokenizeReportIntent(value).map(normalizeReportIntentToken).filter(Boolean).filter((token) => !REPORT_INTENT_STOPWORDS.has(token)).filter((token) => !modifiers.has(token));
|
|
3272
|
+
return [...new Set(tokens)].sort().join(" ");
|
|
3273
|
+
}
|
|
3274
|
+
function tokenizeReportIntent(value) {
|
|
3275
|
+
return value.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
3276
|
+
}
|
|
3277
|
+
function normalizeReportIntentToken(token) {
|
|
3278
|
+
if (token.length > 4 && token.endsWith("ies")) return `${token.slice(0, -3)}y`;
|
|
3279
|
+
if (token.length > 4 && token.endsWith("s") && !token.endsWith("ss")) return token.slice(0, -1);
|
|
3280
|
+
return token;
|
|
2989
3281
|
}
|
|
2990
3282
|
function renderLocationCard(report) {
|
|
2991
3283
|
const location = report.meta.location;
|
|
2992
3284
|
const handling = report.meta.providerLocationHandling;
|
|
2993
3285
|
if (!location && handling.length === 0) return "";
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
};
|
|
3000
|
-
const
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
</
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
<
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
${
|
|
3286
|
+
const otherLocations = location?.otherConfiguredLabels ?? [];
|
|
3287
|
+
const weakLocationProviders = handling.filter((h) => h.treatment === "ignored" || h.treatment === "browser-geo").map((h) => h.provider);
|
|
3288
|
+
const marketValue = location ? locationDisplay(location) : "No market set";
|
|
3289
|
+
const notIncluded = otherLocations.length > 0 ? compactInlineList(otherLocations, 4) : "None";
|
|
3290
|
+
const interpretation = location ? otherLocations.length > 0 ? `${otherLocations.length} configured ${pluralize(otherLocations.length, "market")} still ${otherLocations.length === 1 ? "needs" : "need"} a matching sweep before cross-market recommendations.` : "Single-market report; findings can be read as the current market view." : "No geographic hint was attached to this sweep; read findings as default-market or national results.";
|
|
3291
|
+
const providerCopy = handling.length > 0 ? weakLocationProviders.length > 0 ? `${weakLocationProviders.length} ${pluralize(weakLocationProviders.length, "provider")} need a closer location check.` : `${handling.length} ${pluralize(handling.length, "provider")} received the market context.` : "No provider-level location metadata is available for this report.";
|
|
3292
|
+
const warning = weakLocationProviders.length > 0 ? `<div class="scope-warning">
|
|
3293
|
+
<strong>Location handling needs review</strong>
|
|
3294
|
+
${escapeHtml(compactInlineList(weakLocationProviders, 4))} used weak or indirect market handling. Treat provider-level differences cautiously.
|
|
3295
|
+
</div>` : "";
|
|
3296
|
+
return `<div class="chart-card market-scope-card">
|
|
3297
|
+
<h3>Market Scope</h3>
|
|
3298
|
+
<div class="market-scope-grid">
|
|
3299
|
+
<div class="scope-tile">
|
|
3300
|
+
<div class="scope-label">Current sweep</div>
|
|
3301
|
+
<div class="scope-value">${escapeHtml(marketValue)}</div>
|
|
3302
|
+
<div class="scope-copy">All findings below are scoped to this run.</div>
|
|
3303
|
+
</div>
|
|
3304
|
+
<div class="scope-tile">
|
|
3305
|
+
<div class="scope-label">Not included</div>
|
|
3306
|
+
<div class="scope-value">${escapeHtml(notIncluded)}</div>
|
|
3307
|
+
<div class="scope-copy">${escapeHtml(interpretation)}</div>
|
|
3308
|
+
</div>
|
|
3309
|
+
<div class="scope-tile">
|
|
3310
|
+
<div class="scope-label">Provider context</div>
|
|
3311
|
+
<div class="scope-value">${handling.length > 0 ? formatNumber(handling.length) : "\u2014"}</div>
|
|
3312
|
+
<div class="scope-copy">${escapeHtml(providerCopy)}</div>
|
|
3313
|
+
</div>
|
|
3314
|
+
</div>
|
|
3315
|
+
${warning}
|
|
3024
3316
|
</div>`;
|
|
3025
3317
|
}
|
|
3026
3318
|
function renderExecutiveSummary(report) {
|
|
@@ -3030,6 +3322,36 @@ function renderExecutiveSummary(report) {
|
|
|
3030
3322
|
const queryNoun = s.totalQueryCount === 1 ? "query" : "queries";
|
|
3031
3323
|
const citedFragment = s.totalQueryCount > 0 ? `${s.citedQueryCount}/${s.totalQueryCount} ${queryNoun} cited` : "no queries";
|
|
3032
3324
|
const mentionedFragment = s.totalQueryCount > 0 ? `${s.mentionedQueryCount}/${s.totalQueryCount} ${queryNoun} mentioned` : "no queries";
|
|
3325
|
+
const headlineTitle = s.totalQueryCount > 0 ? `${s.citedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} cite ${report.meta.project.displayName}` : "No AI citation data yet";
|
|
3326
|
+
const headlineSubtitle = s.totalQueryCount > 0 ? `${s.citationRate}% citation coverage and ${s.mentionRate}% mention coverage across ${s.providerCount} ${pluralize(s.providerCount, "provider")}.` : "Run a visibility sweep to populate the first citation and mention baseline.";
|
|
3327
|
+
const priorityActions = report.agencyDiagnostics.priorities.length > 0 ? report.agencyDiagnostics.priorities : report.actionPlan;
|
|
3328
|
+
const actionCount = dedupeReportActions(report, priorityActions).length;
|
|
3329
|
+
const heroHtml = `<div class="executive-hero">
|
|
3330
|
+
<div class="headline-card">
|
|
3331
|
+
<div>
|
|
3332
|
+
<div class="hero-kicker">Latest AI visibility sweep</div>
|
|
3333
|
+
<div class="hero-title">${escapeHtml(headlineTitle)}</div>
|
|
3334
|
+
</div>
|
|
3335
|
+
<div class="hero-subtitle">${escapeHtml(headlineSubtitle)}</div>
|
|
3336
|
+
</div>
|
|
3337
|
+
<div class="hero-proof-grid">
|
|
3338
|
+
<div class="hero-proof">
|
|
3339
|
+
<div class="mini-label">Citation trend</div>
|
|
3340
|
+
<div class="mini-value tone-${trendTone}">${escapeHtml(trendLabel)}</div>
|
|
3341
|
+
<div class="mini-copy">${escapeHtml(citedFragment)}</div>
|
|
3342
|
+
</div>
|
|
3343
|
+
<div class="hero-proof">
|
|
3344
|
+
<div class="mini-label">Mention coverage</div>
|
|
3345
|
+
<div class="mini-value">${s.mentionRate}%</div>
|
|
3346
|
+
<div class="mini-copy">${escapeHtml(mentionedFragment)}</div>
|
|
3347
|
+
</div>
|
|
3348
|
+
<div class="hero-proof">
|
|
3349
|
+
<div class="mini-label">Prioritized actions</div>
|
|
3350
|
+
<div class="mini-value">${formatNumber(actionCount)}</div>
|
|
3351
|
+
<div class="mini-copy">Sorted for agency follow-up.</div>
|
|
3352
|
+
</div>
|
|
3353
|
+
</div>
|
|
3354
|
+
</div>`;
|
|
3033
3355
|
const metrics = [
|
|
3034
3356
|
{
|
|
3035
3357
|
label: "Citation rate",
|
|
@@ -3048,10 +3370,11 @@ function renderExecutiveSummary(report) {
|
|
|
3048
3370
|
}
|
|
3049
3371
|
];
|
|
3050
3372
|
if (s.gsc) {
|
|
3373
|
+
const dateRange = gscDateRange(report);
|
|
3051
3374
|
metrics.push({
|
|
3052
3375
|
label: "GSC clicks",
|
|
3053
3376
|
value: formatNumber(s.gsc.clicks),
|
|
3054
|
-
delta: `${formatNumber(s.gsc.impressions)} imp \xB7 ${formatRatio(s.gsc.ctr)} CTR`
|
|
3377
|
+
delta: `${formatNumber(s.gsc.impressions)} imp \xB7 ${formatRatio(s.gsc.ctr)} CTR${dateRange ? ` \xB7 ${escapeHtml(dateRange)}` : ""}`
|
|
3055
3378
|
});
|
|
3056
3379
|
}
|
|
3057
3380
|
if (s.ga) {
|
|
@@ -3079,9 +3402,9 @@ function renderExecutiveSummary(report) {
|
|
|
3079
3402
|
id: "executive-summary",
|
|
3080
3403
|
eyebrow: "Section 1",
|
|
3081
3404
|
title: "Executive Summary",
|
|
3082
|
-
intro: "
|
|
3405
|
+
intro: "Citation = source list. Mention = answer text. They are independent signals."
|
|
3083
3406
|
},
|
|
3084
|
-
metricsHtml + findingsHtml + locationHtml
|
|
3407
|
+
heroHtml + metricsHtml + findingsHtml + locationHtml
|
|
3085
3408
|
);
|
|
3086
3409
|
}
|
|
3087
3410
|
function renderProviderBars(rates) {
|
|
@@ -3127,7 +3450,7 @@ function renderCitationMatrix(scorecard) {
|
|
|
3127
3450
|
}).join("");
|
|
3128
3451
|
return `<tr><td>${escapeHtml(q)}</td>${cells}</tr>`;
|
|
3129
3452
|
}).join("");
|
|
3130
|
-
const legend = '<p class="section-intro" style="margin-top:0;font-size:11px;">
|
|
3453
|
+
const legend = '<p class="section-intro" style="margin-top:0;font-size:11px;">Legend: <span class="cell-cited">C</span>/<span class="cell-not-cited">c</span> = cited/not, <span class="cell-cited">M</span>/<span class="cell-not-cited">m</span> = mentioned/not, <span class="cell-pending">\u2013</span> = no data.</p>';
|
|
3131
3454
|
return `${legend}<table class="report-table">
|
|
3132
3455
|
<thead><tr><th>Query</th>${headers}</tr></thead>
|
|
3133
3456
|
<tbody>${rows}</tbody>
|
|
@@ -3139,7 +3462,7 @@ function renderCitationScorecard(report) {
|
|
|
3139
3462
|
${renderCitationMatrix(report.citationScorecard)}
|
|
3140
3463
|
`;
|
|
3141
3464
|
return section(
|
|
3142
|
-
{ id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "
|
|
3465
|
+
{ id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "Provider-by-provider citation and mention coverage for the latest sweep." },
|
|
3143
3466
|
body
|
|
3144
3467
|
);
|
|
3145
3468
|
}
|
|
@@ -3221,7 +3544,7 @@ function renderCompetitorLandscape(report) {
|
|
|
3221
3544
|
id: "competitor-landscape",
|
|
3222
3545
|
eyebrow: "Section 3",
|
|
3223
3546
|
title: "Competitor Landscape",
|
|
3224
|
-
intro: "
|
|
3547
|
+
intro: "Who AI engines cite and mention instead of the client."
|
|
3225
3548
|
},
|
|
3226
3549
|
`${charts}${table}`
|
|
3227
3550
|
);
|
|
@@ -3262,6 +3585,26 @@ function renderCategoryBars(buckets) {
|
|
|
3262
3585
|
<div class="source-bars">${rows}</div>
|
|
3263
3586
|
</div>`;
|
|
3264
3587
|
}
|
|
3588
|
+
function renderShareBars(heading, rows, countLabel) {
|
|
3589
|
+
const visibleRows = rows.filter((r) => r.count > 0 || r.sharePct > 0);
|
|
3590
|
+
if (visibleRows.length === 0) return "";
|
|
3591
|
+
const bars = visibleRows.map((r, index) => {
|
|
3592
|
+
const pct = Math.max(0, Math.min(100, r.sharePct));
|
|
3593
|
+
const color = r.color ?? COLORS.series[index % COLORS.series.length];
|
|
3594
|
+
return `
|
|
3595
|
+
<div class="source-bar-row">
|
|
3596
|
+
<div class="source-bar-label">${escapeHtml(r.label)}</div>
|
|
3597
|
+
<div class="source-bar-track">
|
|
3598
|
+
<div class="source-bar-fill" style="width:${pct.toFixed(1)}%;background:${color}"></div>
|
|
3599
|
+
</div>
|
|
3600
|
+
<div class="source-bar-value">${formatNumber(r.count)} <span class="source-bar-pct">${escapeHtml(countLabel)} \xB7 ${r.sharePct}%</span></div>
|
|
3601
|
+
</div>`;
|
|
3602
|
+
}).join("");
|
|
3603
|
+
return `<div class="chart-card">
|
|
3604
|
+
<h3>${escapeHtml(heading)}</h3>
|
|
3605
|
+
<div class="source-bars">${bars}</div>
|
|
3606
|
+
</div>`;
|
|
3607
|
+
}
|
|
3265
3608
|
function renderAiSourceOrigin(report) {
|
|
3266
3609
|
const origin = report.aiSourceOrigin;
|
|
3267
3610
|
if (origin.categories.length === 0 && origin.topDomains.length === 0) {
|
|
@@ -3289,7 +3632,7 @@ function renderAiSourceOrigin(report) {
|
|
|
3289
3632
|
id: "ai-source-origin",
|
|
3290
3633
|
eyebrow: "Section 4",
|
|
3291
3634
|
title: "AI Citation Sources",
|
|
3292
|
-
intro: "
|
|
3635
|
+
intro: "External domains AI engines trusted most in the latest sweep."
|
|
3293
3636
|
},
|
|
3294
3637
|
`${headlineFragment}${table}${renderCategoryBars(origin.categories)}`
|
|
3295
3638
|
);
|
|
@@ -3343,13 +3686,16 @@ function renderGsc(report) {
|
|
|
3343
3686
|
<td class="numeric">${q.avgPosition.toFixed(1)}</td>
|
|
3344
3687
|
<td><span class="badge tone-neutral">${escapeHtml(q.category)}</span></td>
|
|
3345
3688
|
</tr>`).join("");
|
|
3346
|
-
const
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3689
|
+
const categoryBars = renderShareBars(
|
|
3690
|
+
"Search demand by intent",
|
|
3691
|
+
gsc.categoryBreakdown.map((c, index) => ({
|
|
3692
|
+
label: c.category,
|
|
3693
|
+
count: c.clicks,
|
|
3694
|
+
sharePct: c.sharePct,
|
|
3695
|
+
color: COLORS.series[index % COLORS.series.length]
|
|
3696
|
+
})),
|
|
3697
|
+
"clicks"
|
|
3698
|
+
);
|
|
3353
3699
|
const trendChart = renderLineChart(
|
|
3354
3700
|
gsc.trend.map((t) => ({ x: t.date, y: t.clicks, label: t.date.slice(5) })),
|
|
3355
3701
|
COLORS.accent,
|
|
@@ -3358,18 +3704,19 @@ function renderGsc(report) {
|
|
|
3358
3704
|
const crossoverBlocks = [];
|
|
3359
3705
|
if (gsc.trackedButNoGsc.length > 0) {
|
|
3360
3706
|
crossoverBlocks.push(`<div class="chart-card"><h3>AEO queries without search demand</h3>
|
|
3361
|
-
<p class="section-intro">
|
|
3362
|
-
|
|
3707
|
+
<p class="section-intro">Review whether these still belong in the tracking set.</p>
|
|
3708
|
+
${renderProofChips(gsc.trackedButNoGsc, 6)}
|
|
3363
3709
|
</div>`);
|
|
3364
3710
|
}
|
|
3365
3711
|
if (gsc.gscButNotTracked.length > 0) {
|
|
3366
3712
|
crossoverBlocks.push(`<div class="chart-card"><h3>Search queries you should track</h3>
|
|
3367
|
-
<p class="section-intro">
|
|
3368
|
-
|
|
3713
|
+
<p class="section-intro">High-impression candidates to add to AEO tracking.</p>
|
|
3714
|
+
${renderProofChips(gsc.gscButNotTracked, 6)}
|
|
3369
3715
|
</div>`);
|
|
3370
3716
|
}
|
|
3717
|
+
const dateRange = gscDateRange(report);
|
|
3371
3718
|
return section(
|
|
3372
|
-
{ id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro:
|
|
3719
|
+
{ id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro: `Search demand signals to compare against AI visibility${dateRange ? ` for ${dateRange}` : ""}.` },
|
|
3373
3720
|
`<div class="metric-grid">
|
|
3374
3721
|
<div class="metric"><div class="label">Total clicks</div><div class="value">${formatNumber(gsc.totalClicks)}</div></div>
|
|
3375
3722
|
<div class="metric"><div class="label">Total impressions</div><div class="value">${formatNumber(gsc.totalImpressions)}</div></div>
|
|
@@ -3383,12 +3730,7 @@ function renderGsc(report) {
|
|
|
3383
3730
|
<tbody>${rows}</tbody>
|
|
3384
3731
|
</table>
|
|
3385
3732
|
</div>
|
|
3386
|
-
|
|
3387
|
-
<table class="report-table">
|
|
3388
|
-
<thead><tr><th>Category</th><th class="numeric">Clicks</th><th class="numeric">Imp.</th><th class="numeric">Share</th></tr></thead>
|
|
3389
|
-
<tbody>${breakdownRows}</tbody>
|
|
3390
|
-
</table>
|
|
3391
|
-
</div>
|
|
3733
|
+
${categoryBars}
|
|
3392
3734
|
${crossoverBlocks.join("\n")}`
|
|
3393
3735
|
);
|
|
3394
3736
|
}
|
|
@@ -3407,14 +3749,18 @@ function renderGa(report) {
|
|
|
3407
3749
|
<td class="numeric">${formatNumber(p.users)}</td>
|
|
3408
3750
|
<td class="numeric">${formatNumber(p.organicSessions)}</td>
|
|
3409
3751
|
</tr>`).join("");
|
|
3410
|
-
const
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3752
|
+
const channelBars = renderShareBars(
|
|
3753
|
+
"Channel mix",
|
|
3754
|
+
ga.channelBreakdown.map((c, index) => ({
|
|
3755
|
+
label: c.channel,
|
|
3756
|
+
count: c.sessions,
|
|
3757
|
+
sharePct: c.sharePct,
|
|
3758
|
+
color: COLORS.series[index % COLORS.series.length]
|
|
3759
|
+
})),
|
|
3760
|
+
"sessions"
|
|
3761
|
+
);
|
|
3416
3762
|
return section(
|
|
3417
|
-
{ id: "ga", eyebrow: "Section 6", title: "GA4 Traffic", intro: `
|
|
3763
|
+
{ id: "ga", eyebrow: "Section 6", title: "GA4 Traffic", intro: `Site traffic from ${formatDate(ga.periodStart)} to ${formatDate(ga.periodEnd)}.` },
|
|
3418
3764
|
`<div class="metric-grid">
|
|
3419
3765
|
<div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ga.totalSessions)}</div></div>
|
|
3420
3766
|
<div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ga.totalUsers)}</div></div>
|
|
@@ -3426,12 +3772,7 @@ function renderGa(report) {
|
|
|
3426
3772
|
<tbody>${pageRows}</tbody>
|
|
3427
3773
|
</table>
|
|
3428
3774
|
</div>
|
|
3429
|
-
|
|
3430
|
-
<table class="report-table">
|
|
3431
|
-
<thead><tr><th>Channel</th><th class="numeric">Sessions</th><th class="numeric">Share</th></tr></thead>
|
|
3432
|
-
<tbody>${channelRows}</tbody>
|
|
3433
|
-
</table>
|
|
3434
|
-
</div>`
|
|
3775
|
+
${channelBars}`
|
|
3435
3776
|
);
|
|
3436
3777
|
}
|
|
3437
3778
|
function renderSocial(report) {
|
|
@@ -3442,12 +3783,16 @@ function renderSocial(report) {
|
|
|
3442
3783
|
renderEmpty("No social referral data yet.")
|
|
3443
3784
|
);
|
|
3444
3785
|
}
|
|
3445
|
-
const
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3786
|
+
const channelBars = renderShareBars(
|
|
3787
|
+
"Social channel mix",
|
|
3788
|
+
social.channels.map((c, index) => ({
|
|
3789
|
+
label: c.channelGroup,
|
|
3790
|
+
count: c.sessions,
|
|
3791
|
+
sharePct: c.sharePct,
|
|
3792
|
+
color: COLORS.series[index % COLORS.series.length]
|
|
3793
|
+
})),
|
|
3794
|
+
"sessions"
|
|
3795
|
+
);
|
|
3451
3796
|
const campaignRows = social.topCampaigns.map((c) => `
|
|
3452
3797
|
<tr>
|
|
3453
3798
|
<td>${escapeHtml(c.source)}</td>
|
|
@@ -3455,18 +3800,13 @@ function renderSocial(report) {
|
|
|
3455
3800
|
<td class="numeric">${formatNumber(c.sessions)}</td>
|
|
3456
3801
|
</tr>`).join("");
|
|
3457
3802
|
return section(
|
|
3458
|
-
{ id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals", intro: "
|
|
3803
|
+
{ id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals", intro: "Social traffic split by channel and campaign." },
|
|
3459
3804
|
`<div class="metric-grid">
|
|
3460
3805
|
<div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(social.totalSessions)}</div></div>
|
|
3461
3806
|
<div class="metric"><div class="label">Organic social</div><div class="value">${formatNumber(social.organicSessions)}</div></div>
|
|
3462
3807
|
<div class="metric"><div class="label">Paid social</div><div class="value">${formatNumber(social.paidSessions)}</div></div>
|
|
3463
3808
|
</div>
|
|
3464
|
-
|
|
3465
|
-
<table class="report-table">
|
|
3466
|
-
<thead><tr><th>Channel</th><th class="numeric">Sessions</th><th class="numeric">Share</th></tr></thead>
|
|
3467
|
-
<tbody>${channelRows}</tbody>
|
|
3468
|
-
</table>
|
|
3469
|
-
</div>
|
|
3809
|
+
${channelBars}
|
|
3470
3810
|
<div class="chart-card"><h3>Top campaigns</h3>
|
|
3471
3811
|
<table class="report-table">
|
|
3472
3812
|
<thead><tr><th>Source</th><th>Medium</th><th class="numeric">Sessions</th></tr></thead>
|
|
@@ -3483,13 +3823,16 @@ function renderAiReferrals(report) {
|
|
|
3483
3823
|
renderEmpty("No AI referral traffic detected yet.")
|
|
3484
3824
|
);
|
|
3485
3825
|
}
|
|
3486
|
-
const
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3826
|
+
const sourceBars = renderShareBars(
|
|
3827
|
+
"AI sessions by source",
|
|
3828
|
+
ai.bySource.map((s, index) => ({
|
|
3829
|
+
label: s.source,
|
|
3830
|
+
count: s.sessions,
|
|
3831
|
+
sharePct: s.sharePct,
|
|
3832
|
+
color: COLORS.series[(index + 2) % COLORS.series.length]
|
|
3833
|
+
})),
|
|
3834
|
+
"sessions"
|
|
3835
|
+
);
|
|
3493
3836
|
const pageRows = ai.topLandingPages.map((p) => `
|
|
3494
3837
|
<tr>
|
|
3495
3838
|
<td class="page-cell">${formatLandingPageHtml(p.page)}</td>
|
|
@@ -3502,18 +3845,13 @@ function renderAiReferrals(report) {
|
|
|
3502
3845
|
"AI referral sessions over time"
|
|
3503
3846
|
);
|
|
3504
3847
|
return section(
|
|
3505
|
-
{ id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic", intro: "
|
|
3848
|
+
{ id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic", intro: "Traffic arriving from AI answer engines." },
|
|
3506
3849
|
`<div class="metric-grid">
|
|
3507
3850
|
<div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ai.totalSessions)}</div></div>
|
|
3508
3851
|
<div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ai.totalUsers)}</div></div>
|
|
3509
3852
|
</div>
|
|
3510
3853
|
${trendChart}
|
|
3511
|
-
|
|
3512
|
-
<table class="report-table">
|
|
3513
|
-
<thead><tr><th>Source</th><th class="numeric">Sessions</th><th class="numeric">Users</th><th class="numeric">Share</th></tr></thead>
|
|
3514
|
-
<tbody>${sourceRows}</tbody>
|
|
3515
|
-
</table>
|
|
3516
|
-
</div>
|
|
3854
|
+
${sourceBars}
|
|
3517
3855
|
<div class="chart-card"><h3>Top AI landing pages</h3>
|
|
3518
3856
|
<table class="report-table">
|
|
3519
3857
|
<thead><tr><th>Page</th><th class="numeric">Sessions</th><th class="numeric">Users</th></tr></thead>
|
|
@@ -3548,7 +3886,7 @@ function renderIndexingHealth(report) {
|
|
|
3548
3886
|
}).join("");
|
|
3549
3887
|
const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml(s.label)}: ${s.count}</span>`).join("");
|
|
3550
3888
|
return section(
|
|
3551
|
-
{ id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health", intro: `
|
|
3889
|
+
{ id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health", intro: `Pages absent from ${ih.provider === "google" ? "Google" : "Bing"} are harder for AI engines to retrieve.` },
|
|
3552
3890
|
`<div class="metric-grid">
|
|
3553
3891
|
<div class="metric"><div class="label">Indexed</div><div class="value tone-positive">${formatNumber(ih.indexed)}</div></div>
|
|
3554
3892
|
<div class="metric"><div class="label">Total inspected</div><div class="value">${formatNumber(ih.total)}</div></div>
|
|
@@ -3588,7 +3926,7 @@ function renderCitationsTrend(report) {
|
|
|
3588
3926
|
<td>${t.providerRates.map((r) => `${escapeHtml(r.provider)}: ${r.citationRate}%`).join(" \xB7 ")}</td>
|
|
3589
3927
|
</tr>`).join("");
|
|
3590
3928
|
return section(
|
|
3591
|
-
{ id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time", intro: "Citation
|
|
3929
|
+
{ id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time", intro: "Citation coverage across completed visibility sweeps." },
|
|
3592
3930
|
`${chart}
|
|
3593
3931
|
<div class="chart-card"><h3>Run-by-run breakdown</h3>
|
|
3594
3932
|
<table class="report-table">
|
|
@@ -3611,37 +3949,51 @@ function renderInsights(report) {
|
|
|
3611
3949
|
const tone = severityTone(i.severity);
|
|
3612
3950
|
const countChip = count > 1 ? ` <span class="badge tone-neutral">\xD7 ${count}</span>` : "";
|
|
3613
3951
|
return `<tr>
|
|
3614
|
-
<td><span class="badge tone-${tone}">${escapeHtml(i.severity)}</span></td>
|
|
3615
|
-
<td>${escapeHtml(i.title)}${countChip}</td>
|
|
3616
|
-
<td>${escapeHtml(i.query)}</td>
|
|
3617
|
-
<td>${escapeHtml(i.provider)}</td>
|
|
3618
|
-
<td>${i.recommendation ? escapeHtml(i.recommendation) : '<span class="cell-pending">\u2014</span>'}</td>
|
|
3952
|
+
<td class="col-severity"><span class="badge tone-${tone}">${escapeHtml(reportSeverityLabel(i.severity))}</span></td>
|
|
3953
|
+
<td class="col-title">${escapeHtml(i.title)}${countChip}</td>
|
|
3954
|
+
<td class="col-query">${escapeHtml(i.query)}</td>
|
|
3955
|
+
<td class="col-provider">${escapeHtml(i.provider)}</td>
|
|
3956
|
+
<td class="col-recommendation">${i.recommendation ? escapeHtml(i.recommendation) : '<span class="cell-pending">\u2014</span>'}</td>
|
|
3619
3957
|
</tr>`;
|
|
3620
3958
|
}).join("");
|
|
3621
3959
|
return section(
|
|
3622
|
-
{ id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Regressions
|
|
3623
|
-
`<table class="report-table">
|
|
3624
|
-
<thead><tr
|
|
3960
|
+
{ id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Regressions, gains, and recurring alerts ordered by severity." },
|
|
3961
|
+
`<table class="report-table insights-table">
|
|
3962
|
+
<thead><tr>
|
|
3963
|
+
<th class="col-severity">Severity</th>
|
|
3964
|
+
<th class="col-title">Title</th>
|
|
3965
|
+
<th class="col-query">Query</th>
|
|
3966
|
+
<th class="col-provider">Provider</th>
|
|
3967
|
+
<th class="col-recommendation">Recommendation</th>
|
|
3968
|
+
</tr></thead>
|
|
3625
3969
|
<tbody>${rows}</tbody>
|
|
3626
3970
|
</table>`
|
|
3627
3971
|
);
|
|
3628
3972
|
}
|
|
3629
3973
|
function renderOpportunities(report) {
|
|
3630
|
-
const opps = report
|
|
3974
|
+
const opps = dedupeReportOpportunities(report);
|
|
3631
3975
|
if (opps.length === 0) return "";
|
|
3632
3976
|
const canonical = report.meta.project.canonicalDomain;
|
|
3977
|
+
const highlights = `<div class="opportunity-grid">
|
|
3978
|
+
${opps.slice(0, 3).map((o) => `<article class="opportunity-card">
|
|
3979
|
+
<div class="opportunity-score" title="Opportunity score (0\u2013100, higher = stronger)">${Math.round(o.score)}<span class="opportunity-score-suffix">/100</span></div>
|
|
3980
|
+
<h3>${escapeHtml(o.query)}</h3>
|
|
3981
|
+
<p>${escapeHtml(contentActionLabel(o.action))} \xB7 ${escapeHtml(actionConfidenceLabel(o.actionConfidence))} confidence</p>
|
|
3982
|
+
${renderProofChips(o.drivers, 2)}
|
|
3983
|
+
</article>`).join("")}
|
|
3984
|
+
</div>`;
|
|
3633
3985
|
const rows = opps.slice(0, 10).map((o) => {
|
|
3634
3986
|
const ourPage = o.ourBestPage ? `<a href="${escapeHtml(absolutizeProjectUrl(o.ourBestPage.url, canonical))}">${escapeHtml(o.ourBestPage.url)}</a>` : '<span class="cell-not-cited">No page yet</span>';
|
|
3635
3987
|
const winning = o.winningCompetitor ? `<a href="${escapeHtml(o.winningCompetitor.url)}">${escapeHtml(o.winningCompetitor.domain)}</a>` : '<span class="cell-not-cited">\u2014</span>';
|
|
3636
3988
|
const drivers = o.drivers.length > 0 ? `<ul class="driver-list">${o.drivers.map((d) => `<li>${escapeHtml(d)}</li>`).join("")}</ul>` : '<span class="cell-not-cited">No driver signal yet</span>';
|
|
3637
3989
|
return `<tr>
|
|
3638
3990
|
<td>${escapeHtml(o.query)}</td>
|
|
3639
|
-
<td><span class="badge tone-neutral">${escapeHtml(o.action)}</span></td>
|
|
3640
|
-
<td class="numeric">${Math.round(o.score)}</td>
|
|
3991
|
+
<td><span class="badge tone-neutral">${escapeHtml(contentActionLabel(o.action))}</span></td>
|
|
3992
|
+
<td class="numeric" title="Opportunity score (0\u2013100)">${Math.round(o.score)}</td>
|
|
3641
3993
|
<td>${drivers}</td>
|
|
3642
3994
|
<td>${ourPage}</td>
|
|
3643
3995
|
<td>${winning}</td>
|
|
3644
|
-
<td><span class="badge tone-neutral">${escapeHtml(o.actionConfidence)}</span></td>
|
|
3996
|
+
<td><span class="badge tone-neutral">${escapeHtml(actionConfidenceLabel(o.actionConfidence))}</span></td>
|
|
3645
3997
|
</tr>`;
|
|
3646
3998
|
}).join("");
|
|
3647
3999
|
return section(
|
|
@@ -3649,10 +4001,10 @@ function renderOpportunities(report) {
|
|
|
3649
4001
|
id: "content-opportunities",
|
|
3650
4002
|
eyebrow: "Section 12",
|
|
3651
4003
|
title: "Content Opportunities",
|
|
3652
|
-
intro: "Queries where
|
|
4004
|
+
intro: "Queries where content work has the clearest path to more AI citations. Opportunity score is 0\u2013100, higher = stronger."
|
|
3653
4005
|
},
|
|
3654
|
-
|
|
3655
|
-
<thead><tr><th>Query</th><th>Action</th><th class="numeric">Score</th><th>Why</th><th>Our page</th><th>Winning competitor</th><th>Confidence</th></tr></thead>
|
|
4006
|
+
`${highlights}<table class="report-table">
|
|
4007
|
+
<thead><tr><th>Query</th><th>Action</th><th class="numeric" title="Opportunity score (0\u2013100)">Score</th><th>Why</th><th>Our page</th><th>Winning competitor</th><th>Confidence</th></tr></thead>
|
|
3656
4008
|
<tbody>${rows}</tbody>
|
|
3657
4009
|
</table>`
|
|
3658
4010
|
);
|
|
@@ -3675,7 +4027,7 @@ function renderContentGaps(report) {
|
|
|
3675
4027
|
id: "content-gaps",
|
|
3676
4028
|
eyebrow: "Section 13",
|
|
3677
4029
|
title: "Content Gaps",
|
|
3678
|
-
intro:
|
|
4030
|
+
intro: "Tracked queries where competitors are cited and the client is missing."
|
|
3679
4031
|
},
|
|
3680
4032
|
`<table class="report-table">
|
|
3681
4033
|
<thead><tr><th>Query</th><th class="numeric">Competitors cited</th><th>Domains</th><th class="numeric">Miss rate</th></tr></thead>
|
|
@@ -3687,7 +4039,7 @@ function renderRecommendedNextSteps(report) {
|
|
|
3687
4039
|
const steps = report.recommendedNextSteps;
|
|
3688
4040
|
if (steps.length === 0) {
|
|
3689
4041
|
return section(
|
|
3690
|
-
{ id: "recommended-next-steps", eyebrow: "Section 14", title: "Recommended Next Steps", intro: "Action items bucketed by
|
|
4042
|
+
{ id: "recommended-next-steps", eyebrow: "Section 14", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
|
|
3691
4043
|
renderEmpty("No outstanding actions.")
|
|
3692
4044
|
);
|
|
3693
4045
|
}
|
|
@@ -3698,7 +4050,7 @@ function renderRecommendedNextSteps(report) {
|
|
|
3698
4050
|
<span class="rationale">${escapeHtml(s.rationale)}</span>
|
|
3699
4051
|
</div>`).join("");
|
|
3700
4052
|
return section(
|
|
3701
|
-
{ id: "recommended-next-steps", eyebrow: "Section 14", title: "Recommended Next Steps", intro: "Action items bucketed by
|
|
4053
|
+
{ id: "recommended-next-steps", eyebrow: "Section 14", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
|
|
3702
4054
|
`<div class="steps">${items}</div>`
|
|
3703
4055
|
);
|
|
3704
4056
|
}
|
|
@@ -3708,33 +4060,45 @@ function actionAudienceMatches(action, audience) {
|
|
|
3708
4060
|
function renderActionCards(actions) {
|
|
3709
4061
|
if (actions.length === 0) return renderEmpty("No prioritized actions yet.");
|
|
3710
4062
|
return `<div class="action-card-grid">
|
|
3711
|
-
${actions.map((action) => {
|
|
4063
|
+
${actions.map((action, idx) => {
|
|
3712
4064
|
const tone = reportActionTone(action);
|
|
3713
4065
|
const why = action.why.length > 0 ? `<ul>${action.why.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : "";
|
|
3714
4066
|
const evidence = action.evidence.length > 0 ? `<ul>${action.evidence.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : "";
|
|
4067
|
+
const proof = renderProofChips(action.evidence.length > 0 ? action.evidence : action.why, 3);
|
|
4068
|
+
const details = why || evidence ? `<details class="action-details">
|
|
4069
|
+
<summary>Evidence details</summary>
|
|
4070
|
+
${why ? `<div><strong>Why</strong>${why}</div>` : ""}
|
|
4071
|
+
${evidence ? `<div><strong>Evidence</strong>${evidence}</div>` : ""}
|
|
4072
|
+
</details>` : "";
|
|
3715
4073
|
return `<article class="action-card">
|
|
3716
|
-
<div class="action-
|
|
3717
|
-
<
|
|
3718
|
-
<
|
|
3719
|
-
|
|
4074
|
+
<div class="action-head">
|
|
4075
|
+
<div class="action-rank" title="Impact rank \u2014 1 is the highest-leverage action">${idx + 1}</div>
|
|
4076
|
+
<div>
|
|
4077
|
+
<div class="action-meta">
|
|
4078
|
+
<span class="badge tone-${tone}">${escapeHtml(reportHorizonLabel(action.horizon))}</span>
|
|
4079
|
+
<span class="badge tone-neutral">${escapeHtml(reportActionCategoryLabel(action.category))}</span>
|
|
4080
|
+
<span class="badge tone-neutral">${escapeHtml(reportConfidenceLabel(action.confidence))} confidence</span>
|
|
4081
|
+
</div>
|
|
4082
|
+
<h3>${escapeHtml(action.title)}</h3>
|
|
4083
|
+
</div>
|
|
3720
4084
|
</div>
|
|
3721
|
-
<h3>${escapeHtml(action.title)}</h3>
|
|
3722
4085
|
<p>${escapeHtml(action.action)}</p>
|
|
3723
|
-
${
|
|
3724
|
-
${
|
|
3725
|
-
<div class="success-metric"><strong>
|
|
4086
|
+
${proof}
|
|
4087
|
+
${details}
|
|
4088
|
+
<div class="success-metric"><strong>Win condition:</strong> ${escapeHtml(action.successMetric)}</div>
|
|
3726
4089
|
</article>`;
|
|
3727
4090
|
}).join("")}
|
|
3728
4091
|
</div>`;
|
|
3729
4092
|
}
|
|
3730
4093
|
function renderAudienceActionPlan(report, audience) {
|
|
3731
|
-
const
|
|
4094
|
+
const rawActions = audience === "client" ? report.clientSummary.actionItems : report.agencyDiagnostics.priorities.length > 0 ? report.agencyDiagnostics.priorities : report.actionPlan.filter((a) => actionAudienceMatches(a, audience));
|
|
4095
|
+
const actions = dedupeReportActions(report, rawActions);
|
|
3732
4096
|
return section(
|
|
3733
4097
|
{
|
|
3734
4098
|
id: audience === "client" ? "client-action-plan" : "agency-action-plan",
|
|
3735
4099
|
eyebrow: audience === "client" ? "Client actions" : "Agency actions",
|
|
3736
4100
|
title: audience === "client" ? "What We Recommend Next" : "Agency Action Plan",
|
|
3737
|
-
intro: audience === "client" ? "
|
|
4101
|
+
intro: audience === "client" ? "The short list to approve and execute." : "The highest-leverage work, sorted by urgency and evidence strength."
|
|
3738
4102
|
},
|
|
3739
4103
|
renderActionCards(actions)
|
|
3740
4104
|
);
|
|
@@ -3786,11 +4150,12 @@ function renderClientEvidenceSummary(report) {
|
|
|
3786
4150
|
<ul><li>${formatNumber(report.indexingHealth.indexed)} indexed</li><li>${formatNumber(report.indexingHealth.notIndexed)} not indexed</li></ul>
|
|
3787
4151
|
</div>`);
|
|
3788
4152
|
}
|
|
3789
|
-
|
|
4153
|
+
const opportunities = dedupeReportOpportunities(report);
|
|
4154
|
+
if (opportunities.length > 0) {
|
|
3790
4155
|
evidenceCards.push(`<div class="diagnostic-card tone-caution">
|
|
3791
4156
|
<h3>Content opportunities</h3>
|
|
3792
4157
|
<p>Canonry found topics where better content could improve AI citations.</p>
|
|
3793
|
-
<ul>${
|
|
4158
|
+
<ul>${opportunities.slice(0, 5).map((o) => `<li>${escapeHtml(o.query)}: ${escapeHtml(o.action)} (${Math.round(o.score)})</li>`).join("")}</ul>
|
|
3794
4159
|
</div>`);
|
|
3795
4160
|
}
|
|
3796
4161
|
return section(
|
|
@@ -3804,12 +4169,12 @@ function renderClientEvidenceSummary(report) {
|
|
|
3804
4169
|
);
|
|
3805
4170
|
}
|
|
3806
4171
|
function renderAgencyDiagnostics(report) {
|
|
3807
|
-
const diagnostics = report.agencyDiagnostics.diagnostics;
|
|
4172
|
+
const diagnostics = report.agencyDiagnostics.diagnostics.filter((d) => d.title !== "Location caveat");
|
|
3808
4173
|
const body = diagnostics.length > 0 ? `<div class="diagnostics-grid">
|
|
3809
4174
|
${diagnostics.map((d) => `<div class="diagnostic-card tone-${d.severity}">
|
|
3810
4175
|
<h3>${escapeHtml(d.title)}</h3>
|
|
3811
4176
|
<p>${escapeHtml(d.detail)}</p>
|
|
3812
|
-
${d.evidence
|
|
4177
|
+
${renderProofChips(d.evidence, 3)}
|
|
3813
4178
|
</div>`).join("")}
|
|
3814
4179
|
</div>` : renderEmpty("No agency diagnostics available yet.");
|
|
3815
4180
|
return section(
|
|
@@ -3817,7 +4182,7 @@ function renderAgencyDiagnostics(report) {
|
|
|
3817
4182
|
id: "agency-diagnostics",
|
|
3818
4183
|
eyebrow: "Agency diagnostics",
|
|
3819
4184
|
title: "Technical Diagnostics",
|
|
3820
|
-
intro: "
|
|
4185
|
+
intro: "Fast-read operator flags behind the action plan."
|
|
3821
4186
|
},
|
|
3822
4187
|
body
|
|
3823
4188
|
);
|
|
@@ -3911,6 +4276,7 @@ function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
|
3911
4276
|
ownDomain,
|
|
3912
4277
|
competitors: trackedCompetitors,
|
|
3913
4278
|
candidateQueries,
|
|
4279
|
+
queryIntentModifiers: buildQueryIntentModifiers(project, locationFilter),
|
|
3914
4280
|
inventory,
|
|
3915
4281
|
wpSchemaAudit: /* @__PURE__ */ new Map(),
|
|
3916
4282
|
gaTrafficByPage,
|
|
@@ -3920,6 +4286,74 @@ function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
|
3920
4286
|
inProgressActions: /* @__PURE__ */ new Map()
|
|
3921
4287
|
};
|
|
3922
4288
|
}
|
|
4289
|
+
function buildQueryIntentModifiers(project, locationFilter) {
|
|
4290
|
+
if (locationFilter === void 0 || locationFilter === null) return [];
|
|
4291
|
+
const locations = parseJsonColumn(project.locations, []);
|
|
4292
|
+
const currentLocation = locations.find((location) => location.label === locationFilter);
|
|
4293
|
+
const raw = currentLocation ? [
|
|
4294
|
+
currentLocation.label,
|
|
4295
|
+
currentLocation.city,
|
|
4296
|
+
currentLocation.region,
|
|
4297
|
+
regionAbbreviation(currentLocation.region),
|
|
4298
|
+
currentLocation.country
|
|
4299
|
+
] : [locationFilter];
|
|
4300
|
+
return [...new Set(raw.map((value) => value.trim().toLowerCase()).filter(Boolean))];
|
|
4301
|
+
}
|
|
4302
|
+
function regionAbbreviation(region) {
|
|
4303
|
+
return US_REGION_ABBREVIATIONS[region.trim().toLowerCase()] ?? "";
|
|
4304
|
+
}
|
|
4305
|
+
var US_REGION_ABBREVIATIONS = {
|
|
4306
|
+
alabama: "al",
|
|
4307
|
+
alaska: "ak",
|
|
4308
|
+
arizona: "az",
|
|
4309
|
+
arkansas: "ar",
|
|
4310
|
+
california: "ca",
|
|
4311
|
+
colorado: "co",
|
|
4312
|
+
connecticut: "ct",
|
|
4313
|
+
delaware: "de",
|
|
4314
|
+
florida: "fl",
|
|
4315
|
+
georgia: "ga",
|
|
4316
|
+
hawaii: "hi",
|
|
4317
|
+
idaho: "id",
|
|
4318
|
+
illinois: "il",
|
|
4319
|
+
indiana: "in",
|
|
4320
|
+
iowa: "ia",
|
|
4321
|
+
kansas: "ks",
|
|
4322
|
+
kentucky: "ky",
|
|
4323
|
+
louisiana: "la",
|
|
4324
|
+
maine: "me",
|
|
4325
|
+
maryland: "md",
|
|
4326
|
+
massachusetts: "ma",
|
|
4327
|
+
michigan: "mi",
|
|
4328
|
+
minnesota: "mn",
|
|
4329
|
+
mississippi: "ms",
|
|
4330
|
+
missouri: "mo",
|
|
4331
|
+
montana: "mt",
|
|
4332
|
+
nebraska: "ne",
|
|
4333
|
+
nevada: "nv",
|
|
4334
|
+
"new hampshire": "nh",
|
|
4335
|
+
"new jersey": "nj",
|
|
4336
|
+
"new mexico": "nm",
|
|
4337
|
+
"new york": "ny",
|
|
4338
|
+
"north carolina": "nc",
|
|
4339
|
+
"north dakota": "nd",
|
|
4340
|
+
ohio: "oh",
|
|
4341
|
+
oklahoma: "ok",
|
|
4342
|
+
oregon: "or",
|
|
4343
|
+
pennsylvania: "pa",
|
|
4344
|
+
"rhode island": "ri",
|
|
4345
|
+
"south carolina": "sc",
|
|
4346
|
+
"south dakota": "sd",
|
|
4347
|
+
tennessee: "tn",
|
|
4348
|
+
texas: "tx",
|
|
4349
|
+
utah: "ut",
|
|
4350
|
+
vermont: "vt",
|
|
4351
|
+
virginia: "va",
|
|
4352
|
+
washington: "wa",
|
|
4353
|
+
"west virginia": "wv",
|
|
4354
|
+
wisconsin: "wi",
|
|
4355
|
+
wyoming: "wy"
|
|
4356
|
+
};
|
|
3923
4357
|
function listQueries(db, projectId) {
|
|
3924
4358
|
const rows = db.select({ text: queries.query }).from(queries).where(eq12(queries.projectId, projectId)).all();
|
|
3925
4359
|
return rows.map((r) => r.text);
|
|
@@ -4158,6 +4592,14 @@ var TOP_LANDING_PAGES_LIMIT = 20;
|
|
|
4158
4592
|
var TOP_AI_REFERRAL_PAGES_LIMIT = 10;
|
|
4159
4593
|
var TOP_CAMPAIGN_LIMIT = 10;
|
|
4160
4594
|
var INSIGHT_LOOKBACK_RUNS = 5;
|
|
4595
|
+
var REPORT_WINDOW_DAYS = 30;
|
|
4596
|
+
function windowStartDate(endDate, windowDays) {
|
|
4597
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(endDate);
|
|
4598
|
+
if (!m) return endDate;
|
|
4599
|
+
const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
|
|
4600
|
+
d.setUTCDate(d.getUTCDate() - (windowDays - 1));
|
|
4601
|
+
return d.toISOString().slice(0, 10);
|
|
4602
|
+
}
|
|
4161
4603
|
function safeNum(value) {
|
|
4162
4604
|
if (typeof value === "number") return value;
|
|
4163
4605
|
if (typeof value === "string") {
|
|
@@ -4193,7 +4635,12 @@ function loadQueryLookup(db, projectId) {
|
|
|
4193
4635
|
return { byId };
|
|
4194
4636
|
}
|
|
4195
4637
|
function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, trackedQueries) {
|
|
4196
|
-
const
|
|
4638
|
+
const allRows = db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
|
|
4639
|
+
if (allRows.length === 0) return null;
|
|
4640
|
+
let maxDate = "";
|
|
4641
|
+
for (const r of allRows) if (r.date > maxDate) maxDate = r.date;
|
|
4642
|
+
const startDate = windowStartDate(maxDate, REPORT_WINDOW_DAYS);
|
|
4643
|
+
const rows = allRows.filter((r) => r.date >= startDate && r.date <= maxDate);
|
|
4197
4644
|
if (rows.length === 0) return null;
|
|
4198
4645
|
let totalClicks = 0;
|
|
4199
4646
|
let totalImpressions = 0;
|
|
@@ -4239,11 +4686,15 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
|
|
|
4239
4686
|
sharePct: totalClicks > 0 ? Math.round(agg.clicks / totalClicks * 100) : 0
|
|
4240
4687
|
})).sort((a, b) => b.clicks - a.clicks);
|
|
4241
4688
|
const trend = [...trendAgg.entries()].map(([date, agg]) => ({ date, clicks: agg.clicks, impressions: agg.impressions })).sort((a, b) => a.date.localeCompare(b.date));
|
|
4689
|
+
const periodStart = trend[0]?.date ?? "";
|
|
4690
|
+
const periodEnd = trend.at(-1)?.date ?? "";
|
|
4242
4691
|
const trackedSet = new Set(trackedQueries.map((q) => q.toLowerCase()));
|
|
4243
4692
|
const gscQuerySet = new Set([...queryAgg.keys()].map((q) => q.toLowerCase()));
|
|
4244
4693
|
const trackedButNoGsc = trackedQueries.filter((q) => !gscQuerySet.has(q.toLowerCase())).sort();
|
|
4245
4694
|
const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q, projectDisplayName, canonicalDomain) !== "brand").sort((a, b) => b[1].impressions - a[1].impressions).map(([q]) => q).slice(0, TOP_QUERIES_LIMIT);
|
|
4246
4695
|
return {
|
|
4696
|
+
periodStart,
|
|
4697
|
+
periodEnd,
|
|
4247
4698
|
totalClicks,
|
|
4248
4699
|
totalImpressions,
|
|
4249
4700
|
ctr,
|
|
@@ -4256,14 +4707,24 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
|
|
|
4256
4707
|
};
|
|
4257
4708
|
}
|
|
4258
4709
|
function buildGaSection(db, projectId) {
|
|
4259
|
-
const
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4710
|
+
const windowSummary = db.select().from(gaTrafficWindowSummaries).where(
|
|
4711
|
+
and4(
|
|
4712
|
+
eq13(gaTrafficWindowSummaries.projectId, projectId),
|
|
4713
|
+
eq13(gaTrafficWindowSummaries.windowKey, "30d")
|
|
4714
|
+
)
|
|
4715
|
+
).limit(1).get();
|
|
4716
|
+
const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(eq13(gaTrafficSummaries.projectId, projectId)).orderBy(desc6(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
4717
|
+
const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
|
|
4718
|
+
if (!windowSummary && !fallbackSummary && allSnapshotRows.length === 0) return null;
|
|
4719
|
+
let snapshotMaxDate = "";
|
|
4720
|
+
for (const r of allSnapshotRows) if (r.date > snapshotMaxDate) snapshotMaxDate = r.date;
|
|
4721
|
+
const snapshotStartDate = snapshotMaxDate ? windowStartDate(snapshotMaxDate, REPORT_WINDOW_DAYS) : "";
|
|
4722
|
+
const snapshotRows = snapshotStartDate ? allSnapshotRows.filter((r) => r.date >= snapshotStartDate && r.date <= snapshotMaxDate) : allSnapshotRows;
|
|
4723
|
+
const totalSessions = windowSummary?.totalSessions ?? fallbackSummary?.totalSessions ?? snapshotRows.reduce((s, r) => s + r.sessions, 0);
|
|
4724
|
+
const totalUsers = windowSummary?.totalUsers ?? fallbackSummary?.totalUsers ?? snapshotRows.reduce((s, r) => s + r.users, 0);
|
|
4725
|
+
const totalOrganicSessions = windowSummary?.totalOrganicSessions ?? fallbackSummary?.totalOrganicSessions ?? snapshotRows.reduce((s, r) => s + r.organicSessions, 0);
|
|
4265
4726
|
const pageAgg = /* @__PURE__ */ new Map();
|
|
4266
|
-
let directSessions = 0;
|
|
4727
|
+
let directSessions = windowSummary?.totalDirectSessions ?? 0;
|
|
4267
4728
|
for (const r of snapshotRows) {
|
|
4268
4729
|
const page = r.landingPageNormalized ?? r.landingPage;
|
|
4269
4730
|
const existing = pageAgg.get(page) ?? { sessions: 0, users: 0, organic: 0 };
|
|
@@ -4271,7 +4732,7 @@ function buildGaSection(db, projectId) {
|
|
|
4271
4732
|
existing.users += r.users;
|
|
4272
4733
|
existing.organic += r.organicSessions;
|
|
4273
4734
|
pageAgg.set(page, existing);
|
|
4274
|
-
if (r.directSessions != null) directSessions += r.directSessions;
|
|
4735
|
+
if (!windowSummary && r.directSessions != null) directSessions += r.directSessions;
|
|
4275
4736
|
}
|
|
4276
4737
|
const topLandingPages = [...pageAgg.entries()].map(([page, data]) => ({
|
|
4277
4738
|
page,
|
|
@@ -4299,12 +4760,14 @@ function buildGaSection(db, projectId) {
|
|
|
4299
4760
|
}
|
|
4300
4761
|
}
|
|
4301
4762
|
}
|
|
4763
|
+
const periodStart = windowSummary?.periodStart ?? (snapshotStartDate || fallbackSummary?.periodStart || "");
|
|
4764
|
+
const periodEnd = windowSummary?.periodEnd ?? (snapshotMaxDate || fallbackSummary?.periodEnd || "");
|
|
4302
4765
|
return {
|
|
4303
4766
|
totalSessions,
|
|
4304
4767
|
totalUsers,
|
|
4305
4768
|
totalOrganicSessions,
|
|
4306
|
-
periodStart
|
|
4307
|
-
periodEnd
|
|
4769
|
+
periodStart,
|
|
4770
|
+
periodEnd,
|
|
4308
4771
|
topLandingPages,
|
|
4309
4772
|
channelBreakdown
|
|
4310
4773
|
};
|
|
@@ -4906,17 +5369,6 @@ function buildAgencyDiagnostics(input) {
|
|
|
4906
5369
|
severity: input.contentOpportunities.length > 0 ? "caution" : "neutral",
|
|
4907
5370
|
evidence: input.contentOpportunities.slice(0, 3).map((o) => `${o.query}: ${o.action} (${Math.round(o.score)})`)
|
|
4908
5371
|
});
|
|
4909
|
-
if (input.reportLocation) {
|
|
4910
|
-
diagnostics.push({
|
|
4911
|
-
title: "Location caveat",
|
|
4912
|
-
detail: input.reportLocation.otherConfiguredLabels.length > 0 ? "This report is scoped to the latest run location; other configured locations need separate interpretation." : "This report is scoped to one configured location.",
|
|
4913
|
-
severity: input.reportLocation.otherConfiguredLabels.length > 0 ? "caution" : "neutral",
|
|
4914
|
-
evidence: [
|
|
4915
|
-
`Current location: ${input.reportLocation.label}`,
|
|
4916
|
-
...input.reportLocation.otherConfiguredLabels.length > 0 ? [`Other configured locations: ${compactList(input.reportLocation.otherConfiguredLabels)}`] : []
|
|
4917
|
-
]
|
|
4918
|
-
});
|
|
4919
|
-
}
|
|
4920
5372
|
return {
|
|
4921
5373
|
priorities: input.actionPlan.filter((a) => actionAudienceMatches2(a, "agency")).slice(0, 6),
|
|
4922
5374
|
diagnostics
|
|
@@ -5029,7 +5481,9 @@ function buildProjectReport(db, projectName) {
|
|
|
5029
5481
|
clicks: gscSection.totalClicks,
|
|
5030
5482
|
impressions: gscSection.totalImpressions,
|
|
5031
5483
|
ctr: gscSection.ctr,
|
|
5032
|
-
avgPosition: gscSection.avgPosition
|
|
5484
|
+
avgPosition: gscSection.avgPosition,
|
|
5485
|
+
periodStart: gscSection.periodStart,
|
|
5486
|
+
periodEnd: gscSection.periodEnd
|
|
5033
5487
|
} : null,
|
|
5034
5488
|
ga: gaSection ? {
|
|
5035
5489
|
sessions: gaSection.totalSessions,
|