@ainyc/canonry 3.3.2 → 3.3.8

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.
@@ -5,6 +5,26 @@ description: Weekly and monthly report templates with metric tables, regression/
5
5
 
6
6
  # Reporting Templates
7
7
 
8
+ ## One-Command HTML Report
9
+
10
+ When a client asks for a "current state" or "AEO report" without a specific custom narrative, prefer the bundled report instead of hand-rolling sections:
11
+
12
+ ```bash
13
+ canonry report <project> # writes canonry-report-<project>-YYYY-MM-DD.html in cwd
14
+ canonry report <project> --output dist/aeo.html # custom path
15
+ canonry report <project> --format json # raw payload, useful for narrating in chat
16
+ ```
17
+
18
+ The HTML is self-contained (inline CSS + SVG charts, no network dependencies) and covers: executive summary, per-keyword × per-provider citation matrix, competitor landscape, AI source origin, GSC + GA4 performance, social and AI referrals, indexing health, citations trend, prioritized insights, and recommended next steps. Same payload is available via `GET /api/v1/projects/<name>/report` and the `canonry_report` MCP tool — use `--format json` when you want to summarize specific numbers in a thread instead of attaching the file.
19
+
20
+ Behaviors worth knowing before narrating numbers from the report:
21
+ - `executiveSummary.citationRate` is always sourced from the latest visibility run (completed **or** partial), so it tracks the scorecard table even when the latest sweep had a flaky provider.
22
+ - `citationsTrend` excludes partial runs. A project with only one completed run shows `trend: "unknown"` — never claim a comparison that isn't there.
23
+ - Project ownership and competitor tagging use subdomain-aware matching: `blog.example.com` counts as the project when `example.com` is the canonical domain or in `ownedDomains`; `blog.rival.com` is tagged `isCompetitor: true` when `rival.com` is tracked.
24
+ - AI referral totals dedupe overlapping GA4 attribution dimensions (`session` / `first_user` / `manual_utm`).
25
+
26
+ The hand-rolled templates below are still the right call when the user wants a focused weekly/monthly digest with custom regression and gain narratives that the bundled report doesn't surface.
27
+
8
28
  ## Weekly Report
9
29
 
10
30
  ```
@@ -60,7 +60,7 @@ A canonry engagement follows the same loop regardless of project size:
60
60
  2. **Prioritize** — Triage by impact: indexing gaps → schema gaps → content gaps → keyphrase strategy. Branded-term losses are urgent.
61
61
  3. **Execute** — Apply fixes via the canonry CLI or platform integrations. See `references/canonry-cli.md` for the full command catalog and `references/wordpress-integration.md` for the WordPress workflow.
62
62
  4. **Monitor** — Re-run sweeps weekly. Correlate visibility shifts with deployments and competitor moves.
63
- 5. **Report** — Lead with data, not interpretation: "Lost `<keyword>` on Gemini between <date> and <date> — two competitors moved in. Here's what to fix."
63
+ 5. **Report** — Lead with data, not interpretation: "Lost `<keyword>` on Gemini between <date> and <date> — two competitors moved in. Here's what to fix." For a one-command client-facing summary, run `canonry report <project>` to generate a self-contained HTML bundle (executive summary, citation scorecard, competitor landscape, GSC + GA4 performance, insights). Same payload is available via `--format json` and the `canonry_report` MCP tool.
64
64
 
65
65
  ## Common Starting Points
66
66
 
@@ -84,6 +84,24 @@ Output shows:
84
84
  - `✗ not-cited` — domain did not appear
85
85
  - Summary: `Cited: X / Y`
86
86
 
87
+ ## Reports
88
+
89
+ ```bash
90
+ canonry report <project> # write canonry-report-<project>-YYYY-MM-DD.html
91
+ canonry report <project> --output dist/aeo.html # custom path
92
+ canonry report <project> --format json # raw report payload to stdout
93
+ ```
94
+
95
+ One-command client-facing AEO report. Bundles the latest visibility sweep, competitor landscape, AI source origin, GSC + GA4 performance, social and AI referrals, indexing health, citations trend, prioritized insights, and recommended next steps into a self-contained HTML file (inline CSS + SVG charts, no network dependencies). Backed by `GET /api/v1/projects/<name>/report` and the `canonry_report` MCP tool.
96
+
97
+ Behavior to know when narrating numbers from the report:
98
+ - `executiveSummary.citationRate` is sourced from the latest visibility run (completed **or** partial), so it always matches the scorecard table.
99
+ - `citationsTrend` excludes partial runs to avoid skew. A project with only one completed run gets `trend: "unknown"` and the finding "No prior run to compare against." — not "Flat compared to the previous run."
100
+ - Project ownership uses subdomain-aware matching against `project.canonicalDomain` plus any configured `ownedDomains`. `blog.example.com` and `brand.io` count as the project, not as external sources, when those rules apply.
101
+ - Competitor tagging in `aiSourceOrigin.topDomains` uses the same subdomain-aware match — `blog.rival.com` is `isCompetitor: true` when `rival.com` is tracked.
102
+ - AI referral totals dedupe overlapping GA4 attribution dimensions (`session` / `first_user` / `manual_utm`) by picking the largest dimension per `(date, source, medium)`. Two 10-session rows for the same tuple report 10 sessions, not 20.
103
+ - GSC top-query CTR and avgPosition are impression-weighted, matching GSC's own metric semantics across multi-row queries.
104
+
87
105
  ## Analytics
88
106
 
89
107
  ```bash
@@ -1488,7 +1488,9 @@ function extractAnswerMentions(answerText, displayName, domains) {
1488
1488
  const answerBrandKey = brandKeyFromText(answerText);
1489
1489
  const normalizedCandidates = brandNormalizedCandidates(displayName);
1490
1490
  const brandKeyCandidates = brandKeyCandidatesForMatch(displayName);
1491
- const matchesNormalized = normalizedCandidates.some((c) => answerNormalized.includes(c));
1491
+ const matchesNormalized = normalizedCandidates.some(
1492
+ (c) => new RegExp(`\\b${escapeRegExp(c)}\\b`).test(answerNormalized)
1493
+ );
1492
1494
  const matchesBrandKey = brandKeyCandidates.some(
1493
1495
  (c) => c.length >= MIN_BRAND_KEY_LENGTH && answerBrandKey.includes(c)
1494
1496
  );
@@ -1562,7 +1564,6 @@ function brandNormalizedCandidates(displayName) {
1562
1564
  if (!original) return [];
1563
1565
  const stripped = stripBusinessSuffix(original, " ");
1564
1566
  if (!stripped || stripped === original) return [original];
1565
- if (brandKeyFromText(stripped).length < MIN_BRAND_KEY_LENGTH) return [original];
1566
1567
  return [original, stripped];
1567
1568
  }
1568
1569
  function brandKeyCandidatesForMatch(displayName) {
@@ -2676,6 +2677,9 @@ var ApiClient = class {
2676
2677
  `/projects/${encodeURIComponent(project)}/search?${params.toString()}`
2677
2678
  );
2678
2679
  }
2680
+ async getReport(project) {
2681
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/report`);
2682
+ }
2679
2683
  async runDoctor(opts = {}) {
2680
2684
  const qs = opts.checkIds && opts.checkIds.length > 0 ? `?check=${encodeURIComponent(opts.checkIds.join(","))}` : "";
2681
2685
  const path2 = opts.project ? `/projects/${encodeURIComponent(opts.project)}/doctor${qs}` : `/doctor${qs}`;
@@ -2951,6 +2955,17 @@ var canonryMcpTools = [
2951
2955
  openApiOperations: ["GET /api/v1/projects/{name}/overview"],
2952
2956
  handler: (client, input) => client.getProjectOverview(input.project)
2953
2957
  }),
2958
+ defineTool({
2959
+ name: "canonry_report",
2960
+ title: "Get aggregated AEO report",
2961
+ description: "Returns the full client-facing AEO report bundle for a project \u2014 executive summary, per-keyword \xD7 per-provider citation matrix, competitor landscape, AI source origin, GSC/GA4 performance, social and AI referrals, indexing health, citations trend, prioritized insights, and recommended next steps. Same payload `canonry report <project>` consumes to render the self-contained HTML.",
2962
+ access: "read",
2963
+ tier: "monitoring",
2964
+ inputSchema: projectInputSchema,
2965
+ annotations: readAnnotations(),
2966
+ openApiOperations: ["GET /api/v1/projects/{name}/report"],
2967
+ handler: (client, input) => client.getReport(input.project)
2968
+ }),
2954
2969
  defineTool({
2955
2970
  name: "canonry_search",
2956
2971
  title: "Search project (composite)",