@canonry/canonry 4.136.0 → 4.136.2
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/agent-workspace/skills/canonry/references/canonry-cli.md +4 -0
- package/assets/agent-workspace/skills/canonry/references/indexing.md +26 -12
- package/assets/assets/{AuditHistoryPanel-DKaiIlcO.js → AuditHistoryPanel-DtUGiN21.js} +1 -1
- package/assets/assets/{BacklinksPage-Z2_ws8c2.js → BacklinksPage-BGwtmwfz.js} +1 -1
- package/assets/assets/{ChartPrimitives-CF_ocuvj.js → ChartPrimitives-Df2UC1g3.js} +1 -1
- package/assets/assets/{HistoryPage-DDKO8_D8.js → HistoryPage-AtaMVEUx.js} +1 -1
- package/assets/assets/ProjectPage-OlBjxXNx.js +8 -0
- package/assets/assets/{RunRow-BhtCgyqA.js → RunRow-Cmf-5xVo.js} +1 -1
- package/assets/assets/{RunsPage-B8Nw3wP-.js → RunsPage-BAPy9Yri.js} +1 -1
- package/assets/assets/{SettingsPage-DsmQhytl.js → SettingsPage-BXO7WpTo.js} +1 -1
- package/assets/assets/{TrafficPage-COJs6eRH.js → TrafficPage-IH-wiPu2.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-BI6IwzMQ.js → TrafficSourceDetailPage-CTTN-P52.js} +1 -1
- package/assets/assets/{arrow-left-C-lGPq-u.js → arrow-left-BwzAIeJ_.js} +1 -1
- package/assets/assets/{extract-error-message-8h0hF1I1.js → extract-error-message-BIcIoJLN.js} +1 -1
- package/assets/assets/index-BPpSrJY0.js +210 -0
- package/assets/assets/index-CglduHCN.css +1 -0
- package/assets/assets/{trash-2-CnOZwYKe.js → trash-2-C50OeBAV.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-PXEEWJRV.js → chunk-GGIRLRAY.js} +3093 -399
- package/dist/{chunk-3PFRVEWP.js → chunk-MXR3GI5L.js} +54 -122
- package/dist/{chunk-PAR6P5H6.js → chunk-SMFUCXHI.js} +132 -6
- package/dist/{chunk-TPVMHYHD.js → chunk-YVMQWVNY.js} +291 -206
- package/dist/cli.js +184 -15
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-BMEJ5GFZ.js → intelligence-service-BD6BHTEJ.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +9 -9
- package/assets/assets/ProjectPage-hmNsqBov.js +0 -8
- package/assets/assets/index-B4ksyQXo.css +0 -1
- package/assets/assets/index-BoSfFv1t.js +0 -210
|
@@ -238,13 +238,18 @@ import {
|
|
|
238
238
|
gscCoverageSnapshotDtoSchema,
|
|
239
239
|
gscCoverageSummaryDtoSchema,
|
|
240
240
|
gscDeindexedRowSchema,
|
|
241
|
+
gscDiscoverSitemapsResponseDtoSchema,
|
|
241
242
|
gscPerformanceDailyDtoSchema,
|
|
242
243
|
gscSearchDataDtoSchema,
|
|
243
244
|
gscSiteListResponseDtoSchema,
|
|
244
245
|
gscSitemapListResponseDtoSchema,
|
|
246
|
+
gscSubmitSitemapsRequestDtoSchema,
|
|
247
|
+
gscSubmitSitemapsResponseDtoSchema,
|
|
245
248
|
gscUrlInspectionDtoSchema,
|
|
246
249
|
hasLocationLabel,
|
|
247
250
|
healthSnapshotDtoSchema,
|
|
251
|
+
hostMatchesAnyDomain,
|
|
252
|
+
hostMatchesDomain,
|
|
248
253
|
hostOf,
|
|
249
254
|
indexingRequestResponseDtoSchema,
|
|
250
255
|
internalError,
|
|
@@ -333,6 +338,9 @@ import {
|
|
|
333
338
|
summarizeCheckResults,
|
|
334
339
|
surfaceClassFromCompetitorType,
|
|
335
340
|
surfaceClassLabel,
|
|
341
|
+
textContainsAnyBrandAlias,
|
|
342
|
+
textContainsBrandAlias,
|
|
343
|
+
textContainsDomain,
|
|
336
344
|
trafficBackfillResponseSchema,
|
|
337
345
|
trafficConnectVercelRequestSchema,
|
|
338
346
|
trafficConnectWordpressRequestSchema,
|
|
@@ -365,7 +373,7 @@ import {
|
|
|
365
373
|
wordpressSchemaDeployResultDtoSchema,
|
|
366
374
|
wordpressSchemaStatusResultDtoSchema,
|
|
367
375
|
wordpressStatusDtoSchema
|
|
368
|
-
} from "./chunk-
|
|
376
|
+
} from "./chunk-GGIRLRAY.js";
|
|
369
377
|
|
|
370
378
|
// src/intelligence-service.ts
|
|
371
379
|
import { eq as eq44, desc as desc21, asc as asc9, and as and35, ne as ne6, or as or10, inArray as inArray15, gte as gte12, lte as lte8 } from "drizzle-orm";
|
|
@@ -5463,24 +5471,19 @@ function groupInsights(insights2, keyFn = (i) => JSON.stringify([i.query, i.prov
|
|
|
5463
5471
|
var TRANSACTIONAL_RE = /\b(?:buy|price|pricing|cost|hire|near me|services?|agency|consultant|company)\b/i;
|
|
5464
5472
|
var INFORMATIONAL_RE = /\b(?:what|how|why|when|guide|tutorial|vs|versus|alternatives?|examples?|definition)\b/i;
|
|
5465
5473
|
var MIN_BRAND_TOKEN_LENGTH = 3;
|
|
5466
|
-
function compact(value) {
|
|
5467
|
-
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
5468
|
-
}
|
|
5469
5474
|
function buildBrandTokens(canonicalDomain, brandNames = []) {
|
|
5470
5475
|
const seen = /* @__PURE__ */ new Set();
|
|
5471
|
-
const
|
|
5472
|
-
const stemCompact = compact(stem);
|
|
5476
|
+
const stemCompact = brandKeyFromText(brandLabelFromDomain(canonicalDomain));
|
|
5473
5477
|
if (stemCompact.length >= MIN_BRAND_TOKEN_LENGTH) seen.add(stemCompact);
|
|
5474
5478
|
for (const name of brandNames) {
|
|
5475
5479
|
if (!name) continue;
|
|
5476
|
-
const nameCompact =
|
|
5480
|
+
const nameCompact = brandKeyFromText(name);
|
|
5477
5481
|
if (nameCompact.length >= MIN_BRAND_TOKEN_LENGTH) seen.add(nameCompact);
|
|
5478
5482
|
}
|
|
5479
5483
|
return [...seen];
|
|
5480
5484
|
}
|
|
5481
5485
|
function categorizeQueryByIntent(query, brandTokens) {
|
|
5482
|
-
|
|
5483
|
-
if (brandTokens.length > 0 && brandTokens.some((t) => compactQuery.includes(t))) {
|
|
5486
|
+
if (textContainsAnyBrandAlias(query, brandTokens)) {
|
|
5484
5487
|
return "brand";
|
|
5485
5488
|
}
|
|
5486
5489
|
if (TRANSACTIONAL_RE.test(query)) return "lead-gen";
|
|
@@ -5548,12 +5551,7 @@ function buildCitationScorecard(snapshots, queryLookup) {
|
|
|
5548
5551
|
|
|
5549
5552
|
// ../intelligence/src/domain-matching.ts
|
|
5550
5553
|
function citedDomainBelongsToProject(citedDomain, projectDomains) {
|
|
5551
|
-
|
|
5552
|
-
for (const domain of projectDomains) {
|
|
5553
|
-
const normalized = normalizeProjectDomain(domain);
|
|
5554
|
-
if (candidate === normalized || candidate.endsWith(`.${normalized}`)) return true;
|
|
5555
|
-
}
|
|
5556
|
-
return false;
|
|
5554
|
+
return hostMatchesAnyDomain(citedDomain, projectDomains);
|
|
5557
5555
|
}
|
|
5558
5556
|
|
|
5559
5557
|
// ../intelligence/src/competitor-landscape.ts
|
|
@@ -5575,11 +5573,8 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
|
|
|
5575
5573
|
entry.count++;
|
|
5576
5574
|
if (q) entry.queries.add(q);
|
|
5577
5575
|
}
|
|
5578
|
-
const competitorNorm = normalizeUrlDomain(competitor);
|
|
5579
5576
|
for (const gs of snap.groundingSources) {
|
|
5580
|
-
|
|
5581
|
-
if (!host) continue;
|
|
5582
|
-
if (host === competitorNorm || host.endsWith(`.${competitorNorm}`)) {
|
|
5577
|
+
if (hostMatchesDomain(gs.uri, competitor)) {
|
|
5583
5578
|
const entry = competitorMap.get(competitor);
|
|
5584
5579
|
const pageQueries = entry.pages.get(gs.uri) ?? /* @__PURE__ */ new Set();
|
|
5585
5580
|
if (q) pageQueries.add(q);
|
|
@@ -5613,16 +5608,6 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
|
|
|
5613
5608
|
competitorRows.sort((a, b) => b.citationCount - a.citationCount);
|
|
5614
5609
|
return { projectCitationCount, competitors: competitorRows };
|
|
5615
5610
|
}
|
|
5616
|
-
function normalizeUrlDomain(domain) {
|
|
5617
|
-
return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
5618
|
-
}
|
|
5619
|
-
function extractHostFromUri(uri) {
|
|
5620
|
-
try {
|
|
5621
|
-
return new URL(uri).hostname;
|
|
5622
|
-
} catch {
|
|
5623
|
-
return "";
|
|
5624
|
-
}
|
|
5625
|
-
}
|
|
5626
5611
|
|
|
5627
5612
|
// ../intelligence/src/mention-landscape.ts
|
|
5628
5613
|
function buildMentionLandscape(snapshots, competitorDomains, projectBrandNames, projectDomains, queryLookup) {
|
|
@@ -6040,10 +6025,9 @@ function buildCompetitorPressureScore(snapshots, competitorDomains, totalTracked
|
|
|
6040
6025
|
trend: []
|
|
6041
6026
|
};
|
|
6042
6027
|
}
|
|
6043
|
-
const competitorSet = new Set(competitorDomains);
|
|
6044
6028
|
let overlapCount = 0;
|
|
6045
6029
|
for (const snap of snapshots) {
|
|
6046
|
-
if (snap.competitorOverlap.some((
|
|
6030
|
+
if (snap.competitorOverlap.some((domain) => hostMatchesAnyDomain(domain, competitorDomains))) {
|
|
6047
6031
|
overlapCount++;
|
|
6048
6032
|
}
|
|
6049
6033
|
}
|
|
@@ -6068,7 +6052,7 @@ function buildOverviewCompetitors(snapshots, competitors2, queryLookup) {
|
|
|
6068
6052
|
return competitors2.map((competitor, index2) => {
|
|
6069
6053
|
const citedQuerySet = /* @__PURE__ */ new Set();
|
|
6070
6054
|
for (const snap of snapshots) {
|
|
6071
|
-
if (snap.competitorOverlap.
|
|
6055
|
+
if (snap.competitorOverlap.some((domain) => hostMatchesDomain(domain, competitor.domain)) || snap.citedDomains.some((domain) => hostMatchesDomain(domain, competitor.domain))) {
|
|
6072
6056
|
if (snap.queryId) citedQuerySet.add(snap.queryId);
|
|
6073
6057
|
}
|
|
6074
6058
|
}
|
|
@@ -6215,15 +6199,22 @@ function buildMentionShare(snapshots, options) {
|
|
|
6215
6199
|
let projectMentionSnapshots = 0;
|
|
6216
6200
|
let snapshotsWithAnswerText = 0;
|
|
6217
6201
|
const competitorCounts = /* @__PURE__ */ new Map();
|
|
6218
|
-
|
|
6202
|
+
const competitorAliases = /* @__PURE__ */ new Map();
|
|
6203
|
+
for (const competitor of options.competitors) {
|
|
6204
|
+
competitorCounts.set(competitor.domain, 0);
|
|
6205
|
+
competitorAliases.set(
|
|
6206
|
+
competitor.domain,
|
|
6207
|
+
competitor.brandTokens.filter((alias) => brandKeyFromText(alias).length >= 3)
|
|
6208
|
+
);
|
|
6209
|
+
}
|
|
6219
6210
|
for (const snap of snapshots) {
|
|
6220
6211
|
const text2 = snap.answerText ?? "";
|
|
6221
6212
|
if (text2.length === 0) continue;
|
|
6222
6213
|
snapshotsWithAnswerText++;
|
|
6223
6214
|
if (snap.projectMentioned) projectMentionSnapshots++;
|
|
6224
|
-
const answerBrandKey = brandKeyFromText(text2);
|
|
6225
6215
|
for (const competitor of options.competitors) {
|
|
6226
|
-
|
|
6216
|
+
const aliases = competitorAliases.get(competitor.domain) ?? [];
|
|
6217
|
+
if (textContainsAnyBrandAlias(text2, aliases)) {
|
|
6227
6218
|
competitorCounts.set(competitor.domain, (competitorCounts.get(competitor.domain) ?? 0) + 1);
|
|
6228
6219
|
}
|
|
6229
6220
|
}
|
|
@@ -6266,16 +6257,6 @@ function mentionShareTone(score) {
|
|
|
6266
6257
|
if (score >= 25) return "caution";
|
|
6267
6258
|
return "negative";
|
|
6268
6259
|
}
|
|
6269
|
-
function competitorMentioned(text2, answerBrandKey, brandTokens) {
|
|
6270
|
-
for (const token of brandTokens) {
|
|
6271
|
-
if (token.length < 3) continue;
|
|
6272
|
-
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6273
|
-
if (new RegExp(`\\b${escaped}\\b`, "i").test(text2)) return true;
|
|
6274
|
-
const tokenBrandKey = brandKeyFromText(token);
|
|
6275
|
-
if (tokenBrandKey.length >= 3 && answerBrandKey.includes(tokenBrandKey)) return true;
|
|
6276
|
-
}
|
|
6277
|
-
return false;
|
|
6278
|
-
}
|
|
6279
6260
|
function describe(parts) {
|
|
6280
6261
|
const { score, projectMentionSnapshots, competitorMentionSnapshots, perCompetitor } = parts;
|
|
6281
6262
|
if (projectMentionSnapshots === 0 && competitorMentionSnapshots === 0) {
|
|
@@ -9776,14 +9757,7 @@ async function analyticsRoutes(app) {
|
|
|
9776
9757
|
}
|
|
9777
9758
|
var ANCHOR_SWEEP_SCAN_LIMIT = 60;
|
|
9778
9759
|
function isProviderInfraDomain(uri) {
|
|
9779
|
-
|
|
9780
|
-
const host = new URL(uri).hostname.toLowerCase();
|
|
9781
|
-
for (const blocked of AI_PROVIDER_INFRA_DOMAINS) {
|
|
9782
|
-
if (host === blocked || host.endsWith(`.${blocked}`)) return true;
|
|
9783
|
-
}
|
|
9784
|
-
} catch {
|
|
9785
|
-
}
|
|
9786
|
-
return false;
|
|
9760
|
+
return hostMatchesAnyDomain(uri, AI_PROVIDER_INFRA_DOMAINS);
|
|
9787
9761
|
}
|
|
9788
9762
|
function parseGroundingSources(rawResponse) {
|
|
9789
9763
|
const parsed = parseJsonColumn(rawResponse, {});
|
|
@@ -10215,12 +10189,12 @@ import { and as and9, eq as eq13, desc as desc5, inArray as inArray6 } from "dri
|
|
|
10215
10189
|
var RECENT_RUNS_WINDOW = 5;
|
|
10216
10190
|
function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
10217
10191
|
const projectId = project.id;
|
|
10218
|
-
const ownDomain =
|
|
10192
|
+
const ownDomain = hostOf(project.canonicalDomain) ?? "";
|
|
10219
10193
|
const ownedDomains = project.ownedDomains ?? [];
|
|
10220
|
-
const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(
|
|
10194
|
+
const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map((domain) => hostOf(domain) ?? "")]);
|
|
10221
10195
|
const trackedQueries = listQueries(db, projectId);
|
|
10222
10196
|
const candidateQueryStrings = trackedQueries.filter(isBlogShapedQuery);
|
|
10223
|
-
const trackedCompetitors = listCompetitorDomains(db, projectId).map(
|
|
10197
|
+
const trackedCompetitors = listCompetitorDomains(db, projectId).map((domain) => hostOf(domain) ?? "");
|
|
10224
10198
|
const competitorSet = new Set(trackedCompetitors);
|
|
10225
10199
|
const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW, locationFilter);
|
|
10226
10200
|
const latestRunId = recentRunIds[0] ?? "";
|
|
@@ -10261,7 +10235,7 @@ function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
|
10261
10235
|
}
|
|
10262
10236
|
function loadDomainClasses(db, projectId) {
|
|
10263
10237
|
const rows = db.select({ domain: domainClassifications.domain, competitorType: domainClassifications.competitorType }).from(domainClassifications).where(eq13(domainClassifications.projectId, projectId)).all();
|
|
10264
|
-
return new Map(rows.map((r) => [
|
|
10238
|
+
return new Map(rows.map((r) => [hostOf(r.domain) ?? "", r.competitorType]));
|
|
10265
10239
|
}
|
|
10266
10240
|
function buildQueryIntentModifiers(project, locationFilter) {
|
|
10267
10241
|
if (locationFilter === void 0 || locationFilter === null) return [];
|
|
@@ -10480,21 +10454,22 @@ function aggregateCandidate(opts) {
|
|
|
10480
10454
|
const isLatestRun = snap.runId === opts.latestRunId;
|
|
10481
10455
|
const competitorOverlap = snap.competitorOverlap;
|
|
10482
10456
|
for (const domain of competitorOverlap) {
|
|
10483
|
-
const normalized =
|
|
10484
|
-
|
|
10485
|
-
|
|
10457
|
+
const normalized = hostOf(domain) ?? "";
|
|
10458
|
+
const competitor = findMatchingDomain(normalized, opts.competitorSet);
|
|
10459
|
+
if (!competitor) continue;
|
|
10460
|
+
competitorTally.set(competitor, (competitorTally.get(competitor) ?? 0) + 1);
|
|
10486
10461
|
}
|
|
10487
10462
|
const grounding = extractGroundingSources(snap.rawResponse);
|
|
10488
10463
|
for (const g of grounding) {
|
|
10489
|
-
const domain =
|
|
10464
|
+
const domain = hostOf(g.uri) ?? "";
|
|
10490
10465
|
if (!domain) continue;
|
|
10491
|
-
if (opts.ourDomains
|
|
10466
|
+
if (findMatchingDomain(domain, opts.ourDomains)) {
|
|
10492
10467
|
if (isLatestRun) ourCitedInLatestRun = true;
|
|
10493
10468
|
recordGroundingHit(ourGroundingTally, g, domain, snap.provider);
|
|
10494
10469
|
continue;
|
|
10495
10470
|
}
|
|
10496
10471
|
citedSurfaceTally.set(domain, (citedSurfaceTally.get(domain) ?? 0) + 1);
|
|
10497
|
-
if (!opts.competitorSet
|
|
10472
|
+
if (!findMatchingDomain(domain, opts.competitorSet)) continue;
|
|
10498
10473
|
recordGroundingHit(competitorGroundingTally, g, domain, snap.provider);
|
|
10499
10474
|
}
|
|
10500
10475
|
}
|
|
@@ -10516,6 +10491,9 @@ function aggregateCandidate(opts) {
|
|
|
10516
10491
|
runsOfHistory: new Set(opts.snapshots.map((s) => s.runId)).size
|
|
10517
10492
|
};
|
|
10518
10493
|
}
|
|
10494
|
+
function findMatchingDomain(candidate, domains) {
|
|
10495
|
+
return [...domains].find((domain) => hostMatchesDomain(candidate, domain));
|
|
10496
|
+
}
|
|
10519
10497
|
function recordGroundingHit(tally, g, domain, provider) {
|
|
10520
10498
|
const existing = tally.get(g.uri);
|
|
10521
10499
|
if (existing) {
|
|
@@ -10568,16 +10546,6 @@ function extractGroundingSources(rawResponse) {
|
|
|
10568
10546
|
}
|
|
10569
10547
|
return [];
|
|
10570
10548
|
}
|
|
10571
|
-
function extractHostFromUri2(uri) {
|
|
10572
|
-
try {
|
|
10573
|
-
return new URL(uri).hostname;
|
|
10574
|
-
} catch {
|
|
10575
|
-
return "";
|
|
10576
|
-
}
|
|
10577
|
-
}
|
|
10578
|
-
function normalizeDomain(domain) {
|
|
10579
|
-
return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
10580
|
-
}
|
|
10581
10549
|
function extractPath2(url) {
|
|
10582
10550
|
if (!url) return "";
|
|
10583
10551
|
const trimmed = url.trim();
|
|
@@ -14940,10 +14908,7 @@ function stableUnique(values, normalize) {
|
|
|
14940
14908
|
return result;
|
|
14941
14909
|
}
|
|
14942
14910
|
function matchesHost(value, marketingHosts) {
|
|
14943
|
-
|
|
14944
|
-
return marketingHosts.some(
|
|
14945
|
-
(candidate) => normalized === candidate || normalized.endsWith(`.${candidate}`)
|
|
14946
|
-
);
|
|
14911
|
+
return hostMatchesAnyDomain(value, marketingHosts);
|
|
14947
14912
|
}
|
|
14948
14913
|
function normalizeLandingPage(value) {
|
|
14949
14914
|
return normalizeUrlPath(value) ?? UNKNOWN_LANDING_PAGE;
|
|
@@ -15013,10 +14978,10 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
15013
14978
|
const conditions = [];
|
|
15014
14979
|
if (parsedHostScope.data === "marketing") {
|
|
15015
14980
|
const lowerHost = sql9`lower(${hostColumn})`;
|
|
15016
|
-
const
|
|
14981
|
+
const normalizedHost = sql9`case when ${lowerHost} like 'www.%' then substr(${lowerHost}, 5) else ${lowerHost} end`;
|
|
15017
14982
|
conditions.push(or4(...marketingHosts.flatMap((host) => [
|
|
15018
|
-
sql9`${
|
|
15019
|
-
sql9`${
|
|
14983
|
+
sql9`${normalizedHost} = ${host}`,
|
|
14984
|
+
sql9`${normalizedHost} like ${`%.${host}`}`
|
|
15020
14985
|
])));
|
|
15021
14986
|
}
|
|
15022
14987
|
if (pathPrefix && pathPrefix !== "/") {
|
|
@@ -15273,11 +15238,7 @@ var normalizedPath = (value) => {
|
|
|
15273
15238
|
return normalized.split("?")[0] || "/";
|
|
15274
15239
|
};
|
|
15275
15240
|
var inRange = (date, startDate, endDate) => date >= startDate && date <= endDate;
|
|
15276
|
-
var
|
|
15277
|
-
var matchesMarketingHost = (value, marketingHosts) => {
|
|
15278
|
-
const hostname = normalizedHost(value);
|
|
15279
|
-
return marketingHosts.some((candidate) => hostname === candidate || hostname.endsWith(`.${candidate}`));
|
|
15280
|
-
};
|
|
15241
|
+
var matchesMarketingHost = (value, marketingHosts) => hostMatchesAnyDomain(value, marketingHosts);
|
|
15281
15242
|
var pageMatchesMarketingHost = (value, marketingHosts) => {
|
|
15282
15243
|
return matchesMarketingHost(hostOf(value) ?? "", marketingHosts);
|
|
15283
15244
|
};
|
|
@@ -15357,7 +15318,9 @@ function buildOrganicEvidence(db, projectName, periodDays) {
|
|
|
15357
15318
|
const nativeAcquisition = measurement.acquisition;
|
|
15358
15319
|
const nativeAcquisitionActive = nativeAcquisition.status !== "never-synced";
|
|
15359
15320
|
const marketingHosts = measurement.filters.marketingHosts;
|
|
15360
|
-
const normalizedMarketingHosts = [...new Set(
|
|
15321
|
+
const normalizedMarketingHosts = [...new Set(
|
|
15322
|
+
marketingHosts.map((host) => hostOf(host)).filter((host) => host !== null)
|
|
15323
|
+
)];
|
|
15361
15324
|
const nativeStartDate = nativeAcquisition.periods[0]?.startDate;
|
|
15362
15325
|
const nativeEndDate = nativeAcquisition.periods.at(-1)?.endDate;
|
|
15363
15326
|
const gscCoverage = aggregateCoverage(db.select({
|
|
@@ -15810,7 +15773,7 @@ async function citationRoutes(app) {
|
|
|
15810
15773
|
queryId: s.queryId,
|
|
15811
15774
|
runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
|
|
15812
15775
|
}));
|
|
15813
|
-
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq19(competitors.projectId, project.id)).all().map((c) =>
|
|
15776
|
+
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq19(competitors.projectId, project.id)).all().map((c) => hostOf(c.domain) ?? "").filter((d) => d.length > 0);
|
|
15814
15777
|
const response = computeCitationVisibility({
|
|
15815
15778
|
queries: projectQueries.map((q) => ({ id: q.id, query: q.query })),
|
|
15816
15779
|
snapshots,
|
|
@@ -15884,18 +15847,19 @@ function computeCitationVisibility(input) {
|
|
|
15884
15847
|
totalProviders: providers.length
|
|
15885
15848
|
});
|
|
15886
15849
|
}
|
|
15887
|
-
const competitorSet = new Set(competitorDomains);
|
|
15888
15850
|
const competitorGaps = [];
|
|
15889
15851
|
const queryById = new Map(qs.map((q) => [q.id, q.query]));
|
|
15890
15852
|
for (const snap of latestByPair.values()) {
|
|
15891
15853
|
if (citationStateToCited(snap.citationState)) continue;
|
|
15892
|
-
if (
|
|
15854
|
+
if (competitorDomains.length === 0) continue;
|
|
15893
15855
|
const cited = snap.citedDomains;
|
|
15894
15856
|
const overlap = snap.competitorOverlap;
|
|
15895
15857
|
const candidates = new Set(
|
|
15896
|
-
[...cited, ...overlap].map((d) =>
|
|
15858
|
+
[...cited, ...overlap].map((d) => hostOf(d) ?? "").filter((d) => d.length > 0)
|
|
15859
|
+
);
|
|
15860
|
+
const citingCompetitors = competitorDomains.filter(
|
|
15861
|
+
(competitor) => [...candidates].some((candidate) => hostMatchesDomain(candidate, competitor))
|
|
15897
15862
|
);
|
|
15898
|
-
const citingCompetitors = Array.from(candidates).filter((d) => competitorSet.has(d));
|
|
15899
15863
|
if (citingCompetitors.length === 0) continue;
|
|
15900
15864
|
competitorGaps.push({
|
|
15901
15865
|
queryId: snap.queryId,
|
|
@@ -15937,9 +15901,6 @@ function computeCitationVisibility(input) {
|
|
|
15937
15901
|
status: "ready"
|
|
15938
15902
|
};
|
|
15939
15903
|
}
|
|
15940
|
-
function normalizeDomain2(domain) {
|
|
15941
|
-
return domain.toLowerCase().trim().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
15942
|
-
}
|
|
15943
15904
|
|
|
15944
15905
|
// ../api-routes/src/visibility-stats.ts
|
|
15945
15906
|
import { and as and16, desc as desc9, eq as eq20, inArray as inArray9 } from "drizzle-orm";
|
|
@@ -15949,9 +15910,6 @@ var VISIBILITY_COMPARE_MIN_RUNS = 5;
|
|
|
15949
15910
|
function basketPairKey(queryId, provider) {
|
|
15950
15911
|
return JSON.stringify([queryId, provider]);
|
|
15951
15912
|
}
|
|
15952
|
-
function citedHostMatches(citedHost, competitorHost) {
|
|
15953
|
-
return citedHost === competitorHost || citedHost.endsWith(`.${competitorHost}`);
|
|
15954
|
-
}
|
|
15955
15913
|
function restrict(snapshots, attribution, pairs) {
|
|
15956
15914
|
const out = [];
|
|
15957
15915
|
for (const snap of snapshots) {
|
|
@@ -16032,7 +15990,7 @@ function countPeriod(snaps, competitors2) {
|
|
|
16032
15990
|
if (competitorHosts.length > 0 && snap.citedDomains.length > 0) {
|
|
16033
15991
|
const citedHosts = snap.citedDomains.map((d) => hostOf(d)).filter((h) => h !== null && h.length > 0);
|
|
16034
15992
|
for (const compHost of competitorHosts) {
|
|
16035
|
-
if (citedHosts.some((ch) =>
|
|
15993
|
+
if (citedHosts.some((ch) => hostMatchesDomain(ch, compHost))) competitorCited += 1;
|
|
16036
15994
|
}
|
|
16037
15995
|
}
|
|
16038
15996
|
}
|
|
@@ -17487,7 +17445,9 @@ var SCHEMA_TABLE = {
|
|
|
17487
17445
|
GscDeindexedRowDto: gscDeindexedRowSchema,
|
|
17488
17446
|
GscPerformanceDailyDto: gscPerformanceDailyDtoSchema,
|
|
17489
17447
|
GscSearchDataDto: gscSearchDataDtoSchema,
|
|
17448
|
+
GscDiscoverSitemapsResponseDto: gscDiscoverSitemapsResponseDtoSchema,
|
|
17490
17449
|
GscSiteListResponseDto: gscSiteListResponseDtoSchema,
|
|
17450
|
+
GscSubmitSitemapsResponseDto: gscSubmitSitemapsResponseDtoSchema,
|
|
17491
17451
|
GscSitemapListResponseDto: gscSitemapListResponseDtoSchema,
|
|
17492
17452
|
GscUrlInspectionDto: gscUrlInspectionDtoSchema,
|
|
17493
17453
|
HealthSnapshotDto: healthSnapshotDtoSchema,
|
|
@@ -17910,6 +17870,34 @@ var routeCatalog = [
|
|
|
17910
17870
|
200: rawJsonResponse("OpenAPI document.", looseObjectSchema)
|
|
17911
17871
|
}
|
|
17912
17872
|
},
|
|
17873
|
+
{
|
|
17874
|
+
method: "post",
|
|
17875
|
+
path: "/api/v1/projects/{name}/google/gsc/sitemaps/submit",
|
|
17876
|
+
summary: "Submit GSC sitemaps",
|
|
17877
|
+
description: "Submits sitemap URLs to Google Search Console sequentially. An accepted result means Google accepted the submit/refetch request; it does not indicate indexing.",
|
|
17878
|
+
tags: ["google"],
|
|
17879
|
+
parameters: [nameParameter],
|
|
17880
|
+
requestBody: {
|
|
17881
|
+
required: true,
|
|
17882
|
+
content: {
|
|
17883
|
+
"application/json": {
|
|
17884
|
+
schema: {
|
|
17885
|
+
type: "object",
|
|
17886
|
+
required: ["sitemapUrls"],
|
|
17887
|
+
additionalProperties: false,
|
|
17888
|
+
properties: {
|
|
17889
|
+
sitemapUrls: { type: "array", minItems: 1, maxItems: 50, items: { type: "string", format: "uri", pattern: "^https?://" } }
|
|
17890
|
+
}
|
|
17891
|
+
}
|
|
17892
|
+
}
|
|
17893
|
+
}
|
|
17894
|
+
},
|
|
17895
|
+
responses: {
|
|
17896
|
+
200: jsonResponse("Per-sitemap submission results returned.", "GscSubmitSitemapsResponseDto"),
|
|
17897
|
+
400: errorResponse("Invalid sitemap request, property ownership, or OAuth scope."),
|
|
17898
|
+
404: errorResponse("Project or connection not found.")
|
|
17899
|
+
}
|
|
17900
|
+
},
|
|
17913
17901
|
{
|
|
17914
17902
|
method: "put",
|
|
17915
17903
|
path: "/api/v1/projects/{name}",
|
|
@@ -19487,7 +19475,15 @@ var routeCatalog = [
|
|
|
19487
19475
|
path: "/api/v1/projects/{name}/google/gsc/sitemaps",
|
|
19488
19476
|
summary: "List GSC sitemaps",
|
|
19489
19477
|
tags: ["google"],
|
|
19490
|
-
parameters: [
|
|
19478
|
+
parameters: [
|
|
19479
|
+
nameParameter,
|
|
19480
|
+
{
|
|
19481
|
+
name: "sitemapIndex",
|
|
19482
|
+
in: "query",
|
|
19483
|
+
description: "Optional owned sitemap-index URL. When present, returns that index's child entries.",
|
|
19484
|
+
schema: { type: "string", format: "uri", pattern: "^https?://" }
|
|
19485
|
+
}
|
|
19486
|
+
],
|
|
19491
19487
|
responses: {
|
|
19492
19488
|
200: jsonResponse("GSC sitemaps returned.", "GscSitemapListResponseDto"),
|
|
19493
19489
|
400: errorResponse("Invalid sitemap request."),
|
|
@@ -19501,8 +19497,7 @@ var routeCatalog = [
|
|
|
19501
19497
|
tags: ["google"],
|
|
19502
19498
|
parameters: [nameParameter],
|
|
19503
19499
|
responses: {
|
|
19504
|
-
|
|
19505
|
-
200: rawJsonResponse("Discovered sitemaps and queued run returned.", looseObjectSchema),
|
|
19500
|
+
200: jsonResponse("Discovered sitemaps and queued run returned.", "GscDiscoverSitemapsResponseDto"),
|
|
19506
19501
|
400: errorResponse("Invalid sitemap discovery request."),
|
|
19507
19502
|
404: errorResponse("Project or connection not found.")
|
|
19508
19503
|
}
|
|
@@ -23230,7 +23225,8 @@ function buildGbpSummary(input) {
|
|
|
23230
23225
|
// ../integration-google/src/constants.ts
|
|
23231
23226
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
23232
23227
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
23233
|
-
var GSC_SCOPE = "https://www.googleapis.com/auth/webmasters
|
|
23228
|
+
var GSC_SCOPE = "https://www.googleapis.com/auth/webmasters";
|
|
23229
|
+
var GSC_READONLY_SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
|
|
23234
23230
|
var INDEXING_SCOPE = "https://www.googleapis.com/auth/indexing";
|
|
23235
23231
|
var GSC_API_BASE = "https://www.googleapis.com/webmasters/v3";
|
|
23236
23232
|
var URL_INSPECTION_API = "https://searchconsole.googleapis.com/v1/urlInspection/index:inspect";
|
|
@@ -23480,7 +23476,7 @@ function gscClientLog(level, action, ctx) {
|
|
|
23480
23476
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
23481
23477
|
stream.write(JSON.stringify(entry) + "\n");
|
|
23482
23478
|
}
|
|
23483
|
-
async function
|
|
23479
|
+
async function gscFetchResponse(accessToken, url, opts) {
|
|
23484
23480
|
const method = opts?.method ?? "GET";
|
|
23485
23481
|
const headers = {
|
|
23486
23482
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -23506,26 +23502,50 @@ async function gscFetch(accessToken, url, opts) {
|
|
|
23506
23502
|
const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
|
|
23507
23503
|
throw new GoogleApiError(`GSC API error (${res.status}): ${detail}`, res.status);
|
|
23508
23504
|
}
|
|
23509
|
-
return
|
|
23505
|
+
return res;
|
|
23506
|
+
}
|
|
23507
|
+
async function gscFetchJson(accessToken, url, opts) {
|
|
23508
|
+
const res = await gscFetchResponse(accessToken, url, opts);
|
|
23509
|
+
const text2 = await res.text();
|
|
23510
|
+
if (!text2) {
|
|
23511
|
+
throw new GoogleApiError("GSC API returned an empty response where JSON was expected", res.status);
|
|
23512
|
+
}
|
|
23513
|
+
try {
|
|
23514
|
+
return JSON.parse(text2);
|
|
23515
|
+
} catch {
|
|
23516
|
+
throw new GoogleApiError("GSC API returned invalid JSON", res.status);
|
|
23517
|
+
}
|
|
23510
23518
|
}
|
|
23511
23519
|
async function listSites(accessToken) {
|
|
23512
23520
|
validateAccessToken(accessToken);
|
|
23513
|
-
const data = await
|
|
23521
|
+
const data = await gscFetchJson(
|
|
23514
23522
|
accessToken,
|
|
23515
23523
|
`${GSC_API_BASE}/sites`
|
|
23516
23524
|
);
|
|
23517
23525
|
return data.siteEntry ?? [];
|
|
23518
23526
|
}
|
|
23519
|
-
async function listSitemaps(accessToken, siteUrl) {
|
|
23527
|
+
async function listSitemaps(accessToken, siteUrl, sitemapIndex) {
|
|
23520
23528
|
validateAccessToken(accessToken);
|
|
23521
23529
|
validateSiteUrl(siteUrl);
|
|
23530
|
+
if (sitemapIndex != null) validateUrl(sitemapIndex);
|
|
23522
23531
|
const encodedSiteUrl = encodeURIComponent(siteUrl);
|
|
23523
|
-
const
|
|
23532
|
+
const sitemapIndexQuery = sitemapIndex == null ? "" : `?sitemapIndex=${encodeURIComponent(sitemapIndex)}`;
|
|
23533
|
+
const data = await gscFetchJson(
|
|
23524
23534
|
accessToken,
|
|
23525
|
-
`${GSC_API_BASE}/sites/${encodedSiteUrl}/sitemaps`
|
|
23535
|
+
`${GSC_API_BASE}/sites/${encodedSiteUrl}/sitemaps${sitemapIndexQuery}`
|
|
23526
23536
|
);
|
|
23527
23537
|
return data.sitemap ?? [];
|
|
23528
23538
|
}
|
|
23539
|
+
async function submitSitemap(accessToken, siteUrl, sitemapUrl) {
|
|
23540
|
+
validateAccessToken(accessToken);
|
|
23541
|
+
validateSiteUrl(siteUrl);
|
|
23542
|
+
validateUrl(sitemapUrl);
|
|
23543
|
+
await gscFetchResponse(
|
|
23544
|
+
accessToken,
|
|
23545
|
+
`${GSC_API_BASE}/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(sitemapUrl)}`,
|
|
23546
|
+
{ method: "PUT" }
|
|
23547
|
+
);
|
|
23548
|
+
}
|
|
23529
23549
|
async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
|
|
23530
23550
|
validateAccessToken(accessToken);
|
|
23531
23551
|
validateSiteUrl(siteUrl);
|
|
@@ -23555,7 +23575,7 @@ async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
|
|
|
23555
23575
|
requestBody.dimensionFilterGroups = filters;
|
|
23556
23576
|
}
|
|
23557
23577
|
const encodedSiteUrl = encodeURIComponent(siteUrl);
|
|
23558
|
-
const data = await
|
|
23578
|
+
const data = await gscFetchJson(
|
|
23559
23579
|
accessToken,
|
|
23560
23580
|
`${GSC_API_BASE}/sites/${encodedSiteUrl}/searchAnalytics/query`,
|
|
23561
23581
|
{ method: "POST", body: requestBody }
|
|
@@ -23572,7 +23592,7 @@ async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
|
|
|
23572
23592
|
async function publishUrlNotification(accessToken, url, type = "URL_UPDATED") {
|
|
23573
23593
|
validateAccessToken(accessToken);
|
|
23574
23594
|
validateUrl(url);
|
|
23575
|
-
return
|
|
23595
|
+
return gscFetchJson(
|
|
23576
23596
|
accessToken,
|
|
23577
23597
|
`${INDEXING_API_BASE}/urlNotifications:publish`,
|
|
23578
23598
|
{
|
|
@@ -23585,7 +23605,7 @@ async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
|
|
|
23585
23605
|
validateAccessToken(accessToken);
|
|
23586
23606
|
validateUrl(inspectionUrl);
|
|
23587
23607
|
validateSiteUrl(siteUrl);
|
|
23588
|
-
return
|
|
23608
|
+
return gscFetchJson(
|
|
23589
23609
|
accessToken,
|
|
23590
23610
|
URL_INSPECTION_API,
|
|
23591
23611
|
{
|
|
@@ -24779,11 +24799,9 @@ function stableStringify3(value) {
|
|
|
24779
24799
|
|
|
24780
24800
|
// ../api-routes/src/google.ts
|
|
24781
24801
|
function isOnProjectDomain(url, projectDomain) {
|
|
24782
|
-
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
return false;
|
|
24786
|
-
}
|
|
24802
|
+
const urlHost = hostOf(url);
|
|
24803
|
+
const projectHost = hostOf(projectDomain);
|
|
24804
|
+
return urlHost !== null && projectHost !== null && urlHost === projectHost;
|
|
24787
24805
|
}
|
|
24788
24806
|
function scopesForConnectionType(type) {
|
|
24789
24807
|
switch (type) {
|
|
@@ -24795,6 +24813,31 @@ function scopesForConnectionType(type) {
|
|
|
24795
24813
|
return [GBP_SCOPE];
|
|
24796
24814
|
}
|
|
24797
24815
|
}
|
|
24816
|
+
function isSitemapOwnedByProperty(sitemapUrl, propertyId, canonicalDomain) {
|
|
24817
|
+
let sitemap;
|
|
24818
|
+
try {
|
|
24819
|
+
sitemap = new URL(sitemapUrl);
|
|
24820
|
+
} catch {
|
|
24821
|
+
return false;
|
|
24822
|
+
}
|
|
24823
|
+
if (sitemap.protocol !== "http:" && sitemap.protocol !== "https:") return false;
|
|
24824
|
+
if (/^sc-domain:/i.test(propertyId)) {
|
|
24825
|
+
const domain = propertyId.slice("sc-domain:".length).toLowerCase();
|
|
24826
|
+
return hostMatchesDomain(sitemap.hostname, domain);
|
|
24827
|
+
}
|
|
24828
|
+
if (/^\d+$/.test(propertyId)) {
|
|
24829
|
+
return hostOf(sitemap.hostname) === hostOf(canonicalDomain);
|
|
24830
|
+
}
|
|
24831
|
+
try {
|
|
24832
|
+
const property = new URL(propertyId);
|
|
24833
|
+
if (property.protocol !== "http:" && property.protocol !== "https:") return false;
|
|
24834
|
+
if (sitemap.origin !== property.origin) return false;
|
|
24835
|
+
const prefix = property.pathname.endsWith("/") ? property.pathname : `${property.pathname}/`;
|
|
24836
|
+
return sitemap.pathname === property.pathname || sitemap.pathname.startsWith(prefix);
|
|
24837
|
+
} catch {
|
|
24838
|
+
return false;
|
|
24839
|
+
}
|
|
24840
|
+
}
|
|
24798
24841
|
function signState(payload, secret) {
|
|
24799
24842
|
return crypto19.createHmac("sha256", secret).update(payload).digest("hex");
|
|
24800
24843
|
}
|
|
@@ -25454,17 +25497,83 @@ async function googleRoutes(app, opts) {
|
|
|
25454
25497
|
}
|
|
25455
25498
|
const store = requireConnectionStore();
|
|
25456
25499
|
const project = resolveProject(app.db, request.params.name);
|
|
25500
|
+
const conn = store.getConnection(project.canonicalDomain, "gsc");
|
|
25501
|
+
if (!conn?.propertyId) {
|
|
25502
|
+
throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
|
|
25503
|
+
}
|
|
25504
|
+
const sitemapIndex = request.query.sitemapIndex;
|
|
25505
|
+
if (sitemapIndex && !isSitemapOwnedByProperty(sitemapIndex, conn.propertyId, project.canonicalDomain)) {
|
|
25506
|
+
throw validationError(`sitemapIndex must belong to the configured GSC property "${conn.propertyId}".`);
|
|
25507
|
+
}
|
|
25457
25508
|
try {
|
|
25458
25509
|
const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
25459
25510
|
if (!propertyId) {
|
|
25460
25511
|
throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
|
|
25461
25512
|
}
|
|
25462
|
-
const
|
|
25463
|
-
|
|
25513
|
+
const entries = await listSitemaps(accessToken, propertyId, sitemapIndex);
|
|
25514
|
+
const sitemaps = sitemapIndex ? entries.map((sitemap) => ({ ...sitemap, parentSitemapUrl: sitemapIndex })) : entries;
|
|
25515
|
+
const indexes = sitemaps.filter((sitemap) => sitemap.isSitemapsIndex).length;
|
|
25516
|
+
return {
|
|
25517
|
+
sitemaps,
|
|
25518
|
+
summary: { total: sitemaps.length, indexes, files: sitemaps.length - indexes },
|
|
25519
|
+
preferredSubmissionUrls: indexes > 0 ? sitemaps.filter((sitemap) => sitemap.isSitemapsIndex).map((sitemap) => sitemap.path) : sitemaps.map((sitemap) => sitemap.path)
|
|
25520
|
+
};
|
|
25464
25521
|
} catch (err) {
|
|
25465
25522
|
throw gscErrorToAppError(err, "Failed to list Search Console sitemaps");
|
|
25466
25523
|
}
|
|
25467
25524
|
});
|
|
25525
|
+
app.post("/projects/:name/google/gsc/sitemaps/submit", async (request) => {
|
|
25526
|
+
const parsed = gscSubmitSitemapsRequestDtoSchema.safeParse(request.body ?? {});
|
|
25527
|
+
if (!parsed.success) {
|
|
25528
|
+
throw validationError("sitemapUrls must contain between 1 and 50 valid sitemap URLs");
|
|
25529
|
+
}
|
|
25530
|
+
const sitemapUrls = [...new Set(parsed.data.sitemapUrls)];
|
|
25531
|
+
const project = resolveProject(app.db, request.params.name);
|
|
25532
|
+
const store = requireConnectionStore();
|
|
25533
|
+
const conn = store.getConnection(project.canonicalDomain, "gsc");
|
|
25534
|
+
if (!conn) {
|
|
25535
|
+
throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
|
|
25536
|
+
}
|
|
25537
|
+
if (!conn.propertyId) {
|
|
25538
|
+
throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
|
|
25539
|
+
}
|
|
25540
|
+
const invalidUrls = sitemapUrls.filter((url) => !isSitemapOwnedByProperty(url, conn.propertyId, project.canonicalDomain));
|
|
25541
|
+
if (invalidUrls.length > 0) {
|
|
25542
|
+
throw validationError(`Sitemap URLs must belong to the configured GSC property "${conn.propertyId}". Invalid: ${invalidUrls.slice(0, 5).join(", ")}`);
|
|
25543
|
+
}
|
|
25544
|
+
if (!(conn.scopes ?? []).includes(GSC_SCOPE)) {
|
|
25545
|
+
throw validationError('This GSC connection has the read-only webmasters scope and cannot submit sitemaps. Reconnect with "canonry google connect" to grant the full webmasters scope.');
|
|
25546
|
+
}
|
|
25547
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
25548
|
+
if (!googleClientId || !googleClientSecret) {
|
|
25549
|
+
throw validationError("Google OAuth is not configured");
|
|
25550
|
+
}
|
|
25551
|
+
let accessToken;
|
|
25552
|
+
try {
|
|
25553
|
+
({ accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret));
|
|
25554
|
+
} catch (err) {
|
|
25555
|
+
throw gscErrorToAppError(err, "Failed to authorize Search Console sitemap submission");
|
|
25556
|
+
}
|
|
25557
|
+
const results = [];
|
|
25558
|
+
for (const sitemapUrl of sitemapUrls) {
|
|
25559
|
+
try {
|
|
25560
|
+
await submitSitemap(accessToken, conn.propertyId, sitemapUrl);
|
|
25561
|
+
const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25562
|
+
writeAuditLog(app.db, {
|
|
25563
|
+
projectId: project.id,
|
|
25564
|
+
actor: "api",
|
|
25565
|
+
action: "google.sitemap.submitted",
|
|
25566
|
+
entityType: "sitemap",
|
|
25567
|
+
entityId: sitemapUrl
|
|
25568
|
+
});
|
|
25569
|
+
results.push({ sitemapUrl, status: "accepted", submittedAt });
|
|
25570
|
+
} catch (err) {
|
|
25571
|
+
results.push({ sitemapUrl, status: "error", error: err instanceof Error ? err.message : String(err) });
|
|
25572
|
+
}
|
|
25573
|
+
}
|
|
25574
|
+
const accepted = results.filter((result) => result.status === "accepted").length;
|
|
25575
|
+
return { summary: { total: results.length, accepted, failed: results.length - accepted }, results };
|
|
25576
|
+
});
|
|
25468
25577
|
app.post("/projects/:name/google/gsc/discover-sitemaps", async (request) => {
|
|
25469
25578
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
25470
25579
|
if (!googleClientId || !googleClientSecret) {
|
|
@@ -25487,9 +25596,11 @@ async function googleRoutes(app, opts) {
|
|
|
25487
25596
|
throw gscErrorToAppError(err, "Failed to discover Search Console sitemaps");
|
|
25488
25597
|
}
|
|
25489
25598
|
if (sitemaps.length === 0) {
|
|
25490
|
-
throw validationError(
|
|
25599
|
+
throw validationError(
|
|
25600
|
+
`No sitemaps found for this GSC property. Submit one with "canonry google submit-sitemap ${request.params.name} <url>" first.`
|
|
25601
|
+
);
|
|
25491
25602
|
}
|
|
25492
|
-
const primary = sitemaps.find((s) =>
|
|
25603
|
+
const primary = sitemaps.find((s) => s.isSitemapsIndex) ?? sitemaps[0];
|
|
25493
25604
|
const sitemapUrl = primary.path;
|
|
25494
25605
|
store.updateConnection(project.canonicalDomain, "gsc", {
|
|
25495
25606
|
sitemapUrl,
|
|
@@ -38276,9 +38387,7 @@ function normalizeHost2(host) {
|
|
|
38276
38387
|
return host.trim().toLowerCase().replace(/^www\./, "");
|
|
38277
38388
|
}
|
|
38278
38389
|
function hostMatches(host, domain) {
|
|
38279
|
-
|
|
38280
|
-
const normalizedDomain = normalizeHost2(domain);
|
|
38281
|
-
return normalizedHost2 === normalizedDomain || normalizedHost2.endsWith(`.${normalizedDomain}`);
|
|
38390
|
+
return hostMatchesDomain(host, domain);
|
|
38282
38391
|
}
|
|
38283
38392
|
function utmTokenMatchesDomain(utmSource, domain) {
|
|
38284
38393
|
if (hostMatches(utmSource, domain)) return true;
|
|
@@ -38287,12 +38396,7 @@ function utmTokenMatchesDomain(utmSource, domain) {
|
|
|
38287
38396
|
return Boolean(firstLabel) && normalizedUtm === firstLabel;
|
|
38288
38397
|
}
|
|
38289
38398
|
function hostFromUrl(value) {
|
|
38290
|
-
|
|
38291
|
-
try {
|
|
38292
|
-
return normalizeHost2(new URL(value).hostname);
|
|
38293
|
-
} catch {
|
|
38294
|
-
return null;
|
|
38295
|
-
}
|
|
38399
|
+
return hostOf(value);
|
|
38296
38400
|
}
|
|
38297
38401
|
function utmSourceFromQuery(queryString) {
|
|
38298
38402
|
if (!queryString) return null;
|
|
@@ -38462,13 +38566,9 @@ function sessionWindowBucket(value, windowMs) {
|
|
|
38462
38566
|
if (Number.isNaN(date.getTime())) return value;
|
|
38463
38567
|
return new Date(Math.floor(date.getTime() / windowMs) * windowMs).toISOString();
|
|
38464
38568
|
}
|
|
38465
|
-
function normalizeHost3(host) {
|
|
38466
|
-
if (!host) return null;
|
|
38467
|
-
return host.trim().toLowerCase().replace(/^www\./, "") || null;
|
|
38468
|
-
}
|
|
38469
38569
|
function sameHost(a, b) {
|
|
38470
|
-
const normalizedA =
|
|
38471
|
-
const normalizedB =
|
|
38570
|
+
const normalizedA = hostOf(a);
|
|
38571
|
+
const normalizedB = hostOf(b);
|
|
38472
38572
|
return !!normalizedA && !!normalizedB && normalizedA === normalizedB;
|
|
38473
38573
|
}
|
|
38474
38574
|
function pathFromSameOriginReferer(event) {
|
|
@@ -42064,7 +42164,6 @@ var PLACES_CHECK_BY_ID = Object.fromEntries(
|
|
|
42064
42164
|
);
|
|
42065
42165
|
|
|
42066
42166
|
// ../api-routes/src/doctor/checks/google-auth.ts
|
|
42067
|
-
var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
|
|
42068
42167
|
async function resolveAccessToken(ctx) {
|
|
42069
42168
|
if (!ctx.project) {
|
|
42070
42169
|
return { ok: false, output: skippedNoProject4() };
|
|
@@ -42339,8 +42438,19 @@ var scopesCheck2 = {
|
|
|
42339
42438
|
};
|
|
42340
42439
|
}
|
|
42341
42440
|
const granted = new Set(conn.scopes ?? []);
|
|
42342
|
-
const
|
|
42343
|
-
|
|
42441
|
+
const hasFullGscScope = granted.has(GSC_SCOPE);
|
|
42442
|
+
const hasReadOnlyGscScope = granted.has(GSC_READONLY_SCOPE);
|
|
42443
|
+
const hasIndexingScope = granted.has(INDEXING_SCOPE);
|
|
42444
|
+
if (!hasFullGscScope && !hasReadOnlyGscScope) {
|
|
42445
|
+
return {
|
|
42446
|
+
status: CheckStatuses.fail,
|
|
42447
|
+
code: "google.auth.required-scope-missing",
|
|
42448
|
+
summary: "Neither the full nor read-only Search Console scope is granted, so GSC reads will fail.",
|
|
42449
|
+
remediation: `Reconnect to grant a Search Console scope: \`canonry google connect ${ctx.project.name} --type gsc\`.`,
|
|
42450
|
+
details: { granted: [...granted], missing: [GSC_SCOPE, GSC_READONLY_SCOPE] }
|
|
42451
|
+
};
|
|
42452
|
+
}
|
|
42453
|
+
if (hasFullGscScope && hasIndexingScope) {
|
|
42344
42454
|
return {
|
|
42345
42455
|
status: CheckStatuses.ok,
|
|
42346
42456
|
code: "google.auth.scopes-ok",
|
|
@@ -42349,14 +42459,22 @@ var scopesCheck2 = {
|
|
|
42349
42459
|
details: { granted: [...granted] }
|
|
42350
42460
|
};
|
|
42351
42461
|
}
|
|
42352
|
-
|
|
42353
|
-
|
|
42462
|
+
if (!hasFullGscScope) {
|
|
42463
|
+
const missing = [GSC_SCOPE, ...hasIndexingScope ? [] : [INDEXING_SCOPE]];
|
|
42464
|
+
return {
|
|
42465
|
+
status: CheckStatuses.warn,
|
|
42466
|
+
code: "google.auth.sitemap-write-scope-missing",
|
|
42467
|
+
summary: hasIndexingScope ? "The read-only Search Console scope supports existing GSC reads, but sitemap submission is unavailable." : "The read-only Search Console scope supports existing GSC reads, but sitemap submission and the Indexing API are unavailable.",
|
|
42468
|
+
remediation: `Reconnect to grant the full Search Console webmasters scope: \`canonry google connect ${ctx.project.name} --type gsc\`.`,
|
|
42469
|
+
details: { granted: [...granted], missing }
|
|
42470
|
+
};
|
|
42471
|
+
}
|
|
42354
42472
|
return {
|
|
42355
|
-
status:
|
|
42356
|
-
code:
|
|
42357
|
-
summary:
|
|
42473
|
+
status: CheckStatuses.warn,
|
|
42474
|
+
code: "google.auth.indexing-scope-missing",
|
|
42475
|
+
summary: "Indexing API scope is not granted \u2014 `canonry google request-indexing` will fail.",
|
|
42358
42476
|
remediation: `Reconnect to grant missing scopes: \`canonry google connect ${ctx.project.name} --type gsc\`.`,
|
|
42359
|
-
details: { granted: [...granted], missing }
|
|
42477
|
+
details: { granted: [...granted], missing: [INDEXING_SCOPE] }
|
|
42360
42478
|
};
|
|
42361
42479
|
}
|
|
42362
42480
|
};
|
|
@@ -43710,7 +43828,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
|
|
|
43710
43828
|
if (competitorMap.length === 0) return;
|
|
43711
43829
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
43712
43830
|
for (const entry of competitorMap) {
|
|
43713
|
-
const domain =
|
|
43831
|
+
const domain = hostOf(entry.domain) ?? "";
|
|
43714
43832
|
if (!domain) continue;
|
|
43715
43833
|
db.insert(domainClassifications).values({
|
|
43716
43834
|
id: crypto31.randomUUID(),
|
|
@@ -44377,38 +44495,20 @@ function createLogger(module) {
|
|
|
44377
44495
|
}
|
|
44378
44496
|
|
|
44379
44497
|
// src/citation-utils.ts
|
|
44380
|
-
function domainMatches(domain, canonicalDomain) {
|
|
44381
|
-
const normalized = normalizeProjectDomain(canonicalDomain);
|
|
44382
|
-
const d = normalizeProjectDomain(domain);
|
|
44383
|
-
return d === normalized || d.endsWith(`.${normalized}`);
|
|
44384
|
-
}
|
|
44385
44498
|
function pickProjectCitedDomain(citedDomains, projectDomains) {
|
|
44386
44499
|
for (const cited of citedDomains) {
|
|
44387
|
-
if (projectDomains.some((pd) =>
|
|
44500
|
+
if (projectDomains.some((pd) => hostMatchesDomain(cited, pd))) return cited;
|
|
44388
44501
|
}
|
|
44389
44502
|
return void 0;
|
|
44390
44503
|
}
|
|
44391
44504
|
function determineCitationState(normalized, domains) {
|
|
44392
44505
|
for (const canonicalDomain of domains) {
|
|
44393
|
-
|
|
44394
|
-
if (normalized.citedDomains.some((d) => domainMatches(d, bareDomain))) {
|
|
44506
|
+
if (normalized.citedDomains.some((d) => hostMatchesDomain(d, canonicalDomain))) {
|
|
44395
44507
|
return "cited";
|
|
44396
44508
|
}
|
|
44397
|
-
const lowerDomain = bareDomain.toLowerCase();
|
|
44398
44509
|
for (const source of normalized.groundingSources) {
|
|
44399
|
-
|
|
44400
|
-
|
|
44401
|
-
if (lowerDomain.includes(".") && uri.includes(lowerDomain)) {
|
|
44402
|
-
return "cited";
|
|
44403
|
-
}
|
|
44404
|
-
} catch {
|
|
44405
|
-
}
|
|
44406
|
-
if (source.title) {
|
|
44407
|
-
const titleLower = source.title.toLowerCase().replace(/^www\./, "");
|
|
44408
|
-
if (titleLower === lowerDomain || titleLower.endsWith(`.${lowerDomain}`)) {
|
|
44409
|
-
return "cited";
|
|
44410
|
-
}
|
|
44411
|
-
}
|
|
44510
|
+
if (hostMatchesDomain(source.uri, canonicalDomain)) return "cited";
|
|
44511
|
+
if (source.title && hostMatchesDomain(source.title, canonicalDomain)) return "cited";
|
|
44412
44512
|
}
|
|
44413
44513
|
}
|
|
44414
44514
|
return "not-cited";
|
|
@@ -44417,27 +44517,25 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
44417
44517
|
const overlapSet = /* @__PURE__ */ new Set();
|
|
44418
44518
|
for (const d of normalized.citedDomains) {
|
|
44419
44519
|
for (const cd of competitorDomains) {
|
|
44420
|
-
if (
|
|
44520
|
+
if (hostMatchesDomain(d, cd)) {
|
|
44421
44521
|
overlapSet.add(cd);
|
|
44422
44522
|
}
|
|
44423
44523
|
}
|
|
44424
44524
|
}
|
|
44425
44525
|
for (const source of normalized.groundingSources) {
|
|
44426
|
-
const uri = source.uri.toLowerCase();
|
|
44427
44526
|
for (const cd of competitorDomains) {
|
|
44428
|
-
if (uri
|
|
44527
|
+
if (hostMatchesDomain(source.uri, cd)) {
|
|
44429
44528
|
overlapSet.add(cd);
|
|
44430
44529
|
}
|
|
44431
44530
|
}
|
|
44432
44531
|
}
|
|
44433
44532
|
if (normalized.answerText) {
|
|
44434
|
-
const lowerAnswer = normalized.answerText.toLowerCase();
|
|
44435
44533
|
for (const cd of competitorDomains) {
|
|
44436
|
-
if (
|
|
44534
|
+
if (textContainsDomain(normalized.answerText, cd)) {
|
|
44437
44535
|
overlapSet.add(cd);
|
|
44438
44536
|
}
|
|
44439
44537
|
const brand = brandLabelFromDomain(cd);
|
|
44440
|
-
if (brand.length >= 4 &&
|
|
44538
|
+
if (brandKeyFromText(brand).length >= 4 && textContainsBrandAlias(normalized.answerText, brand)) {
|
|
44441
44539
|
overlapSet.add(cd);
|
|
44442
44540
|
}
|
|
44443
44541
|
}
|
|
@@ -44448,27 +44546,24 @@ function computeCitedCompetitorDomains(citedDomains, competitorDomains) {
|
|
|
44448
44546
|
const citedCompetitors = /* @__PURE__ */ new Set();
|
|
44449
44547
|
for (const citedDomain of citedDomains) {
|
|
44450
44548
|
for (const competitorDomain of competitorDomains) {
|
|
44451
|
-
if (
|
|
44549
|
+
if (hostMatchesDomain(citedDomain, competitorDomain)) citedCompetitors.add(competitorDomain);
|
|
44452
44550
|
}
|
|
44453
44551
|
}
|
|
44454
44552
|
return [...citedCompetitors];
|
|
44455
44553
|
}
|
|
44456
|
-
function escapeRegExp5(value) {
|
|
44457
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
44458
|
-
}
|
|
44459
44554
|
function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains, ownBrandNames = []) {
|
|
44460
44555
|
if (!answerText || answerText.length < 20) return [];
|
|
44461
|
-
const
|
|
44462
|
-
ownDomains.flatMap((domain) =>
|
|
44556
|
+
const ownBrandAliases = new Set(
|
|
44557
|
+
ownDomains.flatMap((domain) => collectBrandAliasesFromDomain(domain))
|
|
44463
44558
|
);
|
|
44464
44559
|
for (const name of ownBrandNames) {
|
|
44465
|
-
|
|
44466
|
-
if (key.length >= 4) ownBrandKeys.add(key);
|
|
44560
|
+
if (brandKeyFromText(name).length >= 4) ownBrandAliases.add(name);
|
|
44467
44561
|
}
|
|
44468
|
-
const
|
|
44469
|
-
|
|
44562
|
+
const ownKeys = new Set([...ownBrandAliases].map(brandKeyFromText));
|
|
44563
|
+
const knownCompetitorAliases = new Set(
|
|
44564
|
+
[...citedDomains, ...competitorDomains].flatMap((domain) => collectBrandAliasesFromDomain(domain)).filter((alias) => !ownKeys.has(brandKeyFromText(alias)))
|
|
44470
44565
|
);
|
|
44471
|
-
if (
|
|
44566
|
+
if (knownCompetitorAliases.size === 0) return [];
|
|
44472
44567
|
const candidatePatterns = [
|
|
44473
44568
|
/^\s*(?:[-*]|\d+\.)\s+(?:\*\*)?([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50}?)(?:\*\*)?\s*[:\u2014\u2013-]/gm,
|
|
44474
44569
|
/\*\*([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50})\*\*/g,
|
|
@@ -44518,8 +44613,8 @@ function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, com
|
|
|
44518
44613
|
if (!candidateKey) continue;
|
|
44519
44614
|
if (genericKeys.has(candidateKey)) continue;
|
|
44520
44615
|
if (candidate.split(/\s+/).length > 6) continue;
|
|
44521
|
-
if (
|
|
44522
|
-
if (!
|
|
44616
|
+
if (matchesBrandAlias(candidate, ownBrandAliases)) continue;
|
|
44617
|
+
if (!matchesBrandAlias(candidate, knownCompetitorAliases)) continue;
|
|
44523
44618
|
if (!seen.has(candidateKey)) seen.set(candidateKey, candidate);
|
|
44524
44619
|
}
|
|
44525
44620
|
}
|
|
@@ -44528,27 +44623,17 @@ function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, com
|
|
|
44528
44623
|
function cleanCandidateName(candidate) {
|
|
44529
44624
|
return candidate.replace(/^[\s"'`]+|[\s"'`.,:;!?]+$/g, "").replace(/\s+/g, " ").trim();
|
|
44530
44625
|
}
|
|
44531
|
-
function
|
|
44626
|
+
function collectBrandAliasesFromDomain(domain) {
|
|
44532
44627
|
const reg = registrableDomain(domain);
|
|
44533
|
-
if (!reg)
|
|
44534
|
-
|
|
44535
|
-
|
|
44536
|
-
|
|
44537
|
-
|
|
44538
|
-
|
|
44539
|
-
|
|
44540
|
-
|
|
44541
|
-
|
|
44542
|
-
if (brand.length >= 4) keys.add(brand);
|
|
44543
|
-
return [...keys];
|
|
44544
|
-
}
|
|
44545
|
-
function matchesBrandKey(candidateKey, brandKeys) {
|
|
44546
|
-
for (const brandKey of brandKeys) {
|
|
44547
|
-
if (candidateKey === brandKey) return true;
|
|
44548
|
-
if (candidateKey.startsWith(brandKey) || candidateKey.endsWith(brandKey)) return true;
|
|
44549
|
-
if (brandKey.startsWith(candidateKey) || brandKey.endsWith(candidateKey)) return true;
|
|
44550
|
-
}
|
|
44551
|
-
return false;
|
|
44628
|
+
if (!reg) return [];
|
|
44629
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
44630
|
+
if (brandKeyFromText(reg).length >= 4) aliases.add(reg);
|
|
44631
|
+
const brand = brandLabelFromDomain(reg);
|
|
44632
|
+
if (brandKeyFromText(brand).length >= 4) aliases.add(brand);
|
|
44633
|
+
return [...aliases];
|
|
44634
|
+
}
|
|
44635
|
+
function matchesBrandAlias(candidate, aliases) {
|
|
44636
|
+
return [...aliases].some((alias) => textContainsBrandAlias(candidate, alias));
|
|
44552
44637
|
}
|
|
44553
44638
|
|
|
44554
44639
|
// src/intelligence-service.ts
|