seo 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRIVACY.md +57 -3
- package/dist/cli.js +2 -2
- package/dist/{dist-B1bivk1S.js → dist-GOZ_hkEB.js} +1 -1
- package/dist/{dist-Ban8uP1U.js → dist-U5Io-lSG.js} +104 -88
- package/dist/dist-U5Io-lSG.js.map +1 -0
- package/dist/{esm-CQJgJ8TD.js → esm-Cyusbw2J.js} +2 -2
- package/dist/{esm-CQJgJ8TD.js.map → esm-Cyusbw2J.js.map} +1 -1
- package/dist/index.d.ts +188 -42
- package/dist/index.js +4303 -205
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +412 -58
- package/dist/mcp.js.map +1 -1
- package/package.json +3 -3
- package/dist/dist-Ban8uP1U.js.map +0 -1
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
|
|
2
|
-
import { i as __toESM, n as version, r as __commonJSMin, t as load } from "./esm-
|
|
2
|
+
import { i as __toESM, n as version, r as __commonJSMin, t as load } from "./esm-Cyusbw2J.js";
|
|
3
3
|
import { createHash, randomUUID, sign } from "node:crypto";
|
|
4
4
|
import { ZodOptional, z } from "zod";
|
|
5
5
|
import { arch, homedir, platform, tmpdir, totalmem } from "node:os";
|
|
@@ -13906,13 +13906,13 @@ function bingCall(auth, options) {
|
|
|
13906
13906
|
return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$4 })).d;
|
|
13907
13907
|
};
|
|
13908
13908
|
}
|
|
13909
|
-
function day(value) {
|
|
13909
|
+
function day$2(value) {
|
|
13910
13910
|
if (typeof value !== "string") return null;
|
|
13911
13911
|
const legacy = /^\/Date\((-?\d+)(?:[+-]\d{4})?\)\/$/.exec(value);
|
|
13912
13912
|
const date = legacy ? new Date(Number(legacy[1])) : new Date(value);
|
|
13913
13913
|
return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10);
|
|
13914
13914
|
}
|
|
13915
|
-
const count$
|
|
13915
|
+
const count$2 = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
13916
13916
|
function list$1(data, label) {
|
|
13917
13917
|
if (!Array.isArray(data)) throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster returned an invalid ${label}.`);
|
|
13918
13918
|
return data.filter((item) => Boolean(item) && typeof item === "object");
|
|
@@ -13927,9 +13927,9 @@ async function bingTraffic(call, site) {
|
|
|
13927
13927
|
const rows = [];
|
|
13928
13928
|
let invalidRows = 0;
|
|
13929
13929
|
for (const row of list$1(await call("GetRankAndTrafficStats", { siteUrl: site }), "traffic list")) {
|
|
13930
|
-
const date = day(row.Date);
|
|
13931
|
-
const clicks = count$
|
|
13932
|
-
const impressions = count$
|
|
13930
|
+
const date = day$2(row.Date);
|
|
13931
|
+
const clicks = count$2(row.Clicks);
|
|
13932
|
+
const impressions = count$2(row.Impressions);
|
|
13933
13933
|
if (!date || clicks === null || impressions === null) {
|
|
13934
13934
|
invalidRows += 1;
|
|
13935
13935
|
continue;
|
|
@@ -13950,11 +13950,11 @@ async function keyed(call, method, params) {
|
|
|
13950
13950
|
const rows = [];
|
|
13951
13951
|
let invalidRows = 0;
|
|
13952
13952
|
for (const row of list$1(await call(method, params), "row list")) {
|
|
13953
|
-
const date = day(row.Date);
|
|
13953
|
+
const date = day$2(row.Date);
|
|
13954
13954
|
const key = typeof row.Query === "string" ? row.Query.trim() : "";
|
|
13955
|
-
const clicks = count$
|
|
13956
|
-
const impressions = count$
|
|
13957
|
-
const position = count$
|
|
13955
|
+
const clicks = count$2(row.Clicks);
|
|
13956
|
+
const impressions = count$2(row.Impressions);
|
|
13957
|
+
const position = count$2(row.AvgImpressionPosition);
|
|
13958
13958
|
if (!date || !key || clicks === null || impressions === null) {
|
|
13959
13959
|
invalidRows += 1;
|
|
13960
13960
|
continue;
|
|
@@ -14601,7 +14601,7 @@ const METRIC_COLUMNS = [
|
|
|
14601
14601
|
"score",
|
|
14602
14602
|
"priority.score"
|
|
14603
14603
|
];
|
|
14604
|
-
const SUMMARY_ROWS$
|
|
14604
|
+
const SUMMARY_ROWS$2 = 10;
|
|
14605
14605
|
const CELL_MAX = 80;
|
|
14606
14606
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14607
14607
|
const isPrimitive = (value) => value === null || [
|
|
@@ -14672,7 +14672,7 @@ function cell(value, origin, max = CELL_MAX) {
|
|
|
14672
14672
|
text = text.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
14673
14673
|
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
14674
14674
|
}
|
|
14675
|
-
function table(rows, columns, origin, max = CELL_MAX) {
|
|
14675
|
+
function table$1(rows, columns, origin, max = CELL_MAX) {
|
|
14676
14676
|
if (rows.length === 0 || columns.length === 0) return "";
|
|
14677
14677
|
return [columns.join(" | "), ...rows.map((row) => columns.map((key) => cell(row[key], origin, max)).join(" | "))].join("\n");
|
|
14678
14678
|
}
|
|
@@ -14748,7 +14748,7 @@ function presentReport(input) {
|
|
|
14748
14748
|
text: [
|
|
14749
14749
|
head,
|
|
14750
14750
|
list ? `${list.path}: rows ${input.offset + 1} to ${end} of ${flat.length}.` : "This report has no list of rows. Use detail \"full\".",
|
|
14751
|
-
table(page, columns, origin),
|
|
14751
|
+
table$1(page, columns, origin),
|
|
14752
14752
|
end < flat.length ? `Next page: ${input.more("rows", end)}` : ""
|
|
14753
14753
|
].filter(Boolean).join("\n\n"),
|
|
14754
14754
|
data: {
|
|
@@ -14760,7 +14760,7 @@ function presentReport(input) {
|
|
|
14760
14760
|
}
|
|
14761
14761
|
};
|
|
14762
14762
|
}
|
|
14763
|
-
const shown = flat.slice(0, Math.min(input.limit, SUMMARY_ROWS$
|
|
14763
|
+
const shown = flat.slice(0, Math.min(input.limit, SUMMARY_ROWS$2));
|
|
14764
14764
|
const columns = columnsFor(flat, 8);
|
|
14765
14765
|
return {
|
|
14766
14766
|
text: [
|
|
@@ -14769,7 +14769,7 @@ function presentReport(input) {
|
|
|
14769
14769
|
facts ? `Facts: ${facts}.` : "",
|
|
14770
14770
|
...input.notes ?? [],
|
|
14771
14771
|
flat.length < 5 ? filteredLine(report) : null,
|
|
14772
|
-
list ? `${list.path} (${shown.length} of ${flat.length} shown):\n${table(shown, columns, origin)}` : "",
|
|
14772
|
+
list ? `${list.path} (${shown.length} of ${flat.length} shown):\n${table$1(shown, columns, origin)}` : "",
|
|
14773
14773
|
limits.length ? `Limits:\n${limits.map((line) => `- ${line}`).join("\n")}` : "",
|
|
14774
14774
|
`More: ${shown.length < flat.length ? `${input.more("rows", shown.length)} for more rows, or ` : ""}${input.more("full")} for every field.`
|
|
14775
14775
|
].filter(Boolean).join("\n\n"),
|
|
@@ -14792,7 +14792,7 @@ function preview(report, origin, rows) {
|
|
|
14792
14792
|
const flat = list.rows.map(flatten);
|
|
14793
14793
|
return {
|
|
14794
14794
|
total: flat.length,
|
|
14795
|
-
text: table(flat.slice(0, rows), columnsFor(flat, 6), origin)
|
|
14795
|
+
text: table$1(flat.slice(0, rows), columnsFor(flat, 6), origin)
|
|
14796
14796
|
};
|
|
14797
14797
|
}
|
|
14798
14798
|
//#endregion
|
|
@@ -14847,9 +14847,9 @@ async function runSites(input) {
|
|
|
14847
14847
|
text: [
|
|
14848
14848
|
named ? `Google accounts: ${input.accounts.map((account) => account.email).join(", ")}.` : null,
|
|
14849
14849
|
`Search Console properties (${properties.length}):`,
|
|
14850
|
-
properties.length ? table(properties, columns, null) : "None. These Google sign-ins have no Search Console access, so the reports cannot run.",
|
|
14850
|
+
properties.length ? table$1(properties, columns, null) : "None. These Google sign-ins have no Search Console access, so the reports cannot run.",
|
|
14851
14851
|
...failed.map((account) => `${account.email ?? "Google"}: could not list properties. ${account.properties.message}`),
|
|
14852
|
-
analytics.length ? `Google Analytics properties (${analytics.length}):\n${table(analytics, [
|
|
14852
|
+
analytics.length ? `Google Analytics properties (${analytics.length}):\n${table$1(analytics, [
|
|
14853
14853
|
"property",
|
|
14854
14854
|
"name",
|
|
14855
14855
|
"account",
|
|
@@ -14946,6 +14946,9 @@ var LinkedAccounts = class {
|
|
|
14946
14946
|
})();
|
|
14947
14947
|
return this.owners;
|
|
14948
14948
|
}
|
|
14949
|
+
async properties() {
|
|
14950
|
+
return [...(await this.ownerMap()).keys()].sort();
|
|
14951
|
+
}
|
|
14949
14952
|
async resolve(site) {
|
|
14950
14953
|
if (this.google.length === 0) throw new CommandError("NOT_CONNECTED", `No Google account is connected. ${this.hint.signIn}`);
|
|
14951
14954
|
const found = matchProperty([...(await this.ownerMap()).keys()], site);
|
|
@@ -17173,11 +17176,11 @@ function analyzeAiReferralRows(input) {
|
|
|
17173
17176
|
}
|
|
17174
17177
|
//#endregion
|
|
17175
17178
|
//#region packages/core/dist/analyze/ai-referrals-query.js
|
|
17176
|
-
const PAGE_SIZE$
|
|
17179
|
+
const PAGE_SIZE$4 = 25e3;
|
|
17177
17180
|
async function fetchAiReferralRows(input) {
|
|
17178
|
-
const pageSize = input.pageSize ?? PAGE_SIZE$
|
|
17181
|
+
const pageSize = input.pageSize ?? PAGE_SIZE$4;
|
|
17179
17182
|
if (!Number.isInteger(input.maxRows) || input.maxRows < 1) throw new Error("maxRows must be a positive whole number.");
|
|
17180
|
-
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > PAGE_SIZE$
|
|
17183
|
+
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > PAGE_SIZE$4) throw new Error(`pageSize must be a whole number between 1 and ${PAGE_SIZE$4}.`);
|
|
17181
17184
|
const rows = [];
|
|
17182
17185
|
const warnings = /* @__PURE__ */ new Set();
|
|
17183
17186
|
const retainedKeys = /* @__PURE__ */ new Set();
|
|
@@ -17284,7 +17287,7 @@ function resultLimit(input) {
|
|
|
17284
17287
|
if (!Number.isInteger(value) || value < 1 || value > MAX_RESULT_LIMIT) throw new SeoError("INVALID_INPUT", `resultLimit must be a whole number between 1 and ${MAX_RESULT_LIMIT}.`);
|
|
17285
17288
|
return value;
|
|
17286
17289
|
}
|
|
17287
|
-
function evidence(result) {
|
|
17290
|
+
function evidence$1(result) {
|
|
17288
17291
|
return {
|
|
17289
17292
|
status: result.truncated || result.warnings.length ? "partial" : "complete",
|
|
17290
17293
|
calls: result.calls,
|
|
@@ -17387,7 +17390,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
17387
17390
|
label: "AI referral landing-page detail query",
|
|
17388
17391
|
query
|
|
17389
17392
|
});
|
|
17390
|
-
detailEvidence = evidence(detailResult);
|
|
17393
|
+
detailEvidence = evidence$1(detailResult);
|
|
17391
17394
|
} catch (error) {
|
|
17392
17395
|
detailEvidence = unavailableEvidence(`The landing-page detail query failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
17393
17396
|
}
|
|
@@ -17405,7 +17408,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
17405
17408
|
query
|
|
17406
17409
|
});
|
|
17407
17410
|
totalUsers = totalUsersFromRows(usersResult.rows) ?? null;
|
|
17408
|
-
totalUsersEvidence = evidence(usersResult);
|
|
17411
|
+
totalUsersEvidence = evidence$1(usersResult);
|
|
17409
17412
|
if (totalUsers === null) totalUsersEvidence = unavailableEvidence("The de-duplicated total-users query returned no valid value.");
|
|
17410
17413
|
} catch (error) {
|
|
17411
17414
|
totalUsersEvidence = unavailableEvidence(`The de-duplicated total-users query failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -17420,7 +17423,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
17420
17423
|
sourceRows: sourceResult.rows,
|
|
17421
17424
|
detailRows: detailResult?.rows ?? [],
|
|
17422
17425
|
totalUsers,
|
|
17423
|
-
sourceDiscovery: evidence(sourceResult),
|
|
17426
|
+
sourceDiscovery: evidence$1(sourceResult),
|
|
17424
17427
|
detail: detailEvidence,
|
|
17425
17428
|
totalUsersEvidence
|
|
17426
17429
|
});
|
|
@@ -20316,10 +20319,29 @@ async function urlInspectionQuota(input) {
|
|
|
20316
20319
|
property: input.siteUrl
|
|
20317
20320
|
});
|
|
20318
20321
|
}
|
|
20322
|
+
function reserveOrSkip(input) {
|
|
20323
|
+
try {
|
|
20324
|
+
return reserveUrlInspectionQuota(input);
|
|
20325
|
+
} catch (error) {
|
|
20326
|
+
if (error instanceof SeoError) throw error;
|
|
20327
|
+
return;
|
|
20328
|
+
}
|
|
20329
|
+
}
|
|
20330
|
+
function finalize(reservation, outcome, blockedUntil, now) {
|
|
20331
|
+
if (!reservation) return;
|
|
20332
|
+
try {
|
|
20333
|
+
finalizeUrlInspectionQuota({
|
|
20334
|
+
reservation,
|
|
20335
|
+
outcome,
|
|
20336
|
+
blockedUntil,
|
|
20337
|
+
now
|
|
20338
|
+
});
|
|
20339
|
+
} catch {}
|
|
20340
|
+
}
|
|
20319
20341
|
async function inspectUrl(input) {
|
|
20320
20342
|
const inspectionUrl = assertUrlMatchesGscProperty(input.siteUrl, input.inspectionUrl);
|
|
20321
20343
|
const { client, quotaIdentity } = await getAuthorized(input.accountEmail ?? await cloudAccountFor(input.siteUrl));
|
|
20322
|
-
const reservation =
|
|
20344
|
+
const reservation = reserveOrSkip({
|
|
20323
20345
|
credentialKey: credentialKey(quotaIdentity),
|
|
20324
20346
|
property: input.siteUrl,
|
|
20325
20347
|
limit: input.quotaLimit
|
|
@@ -20335,35 +20357,24 @@ async function inspectUrl(input) {
|
|
|
20335
20357
|
})
|
|
20336
20358
|
});
|
|
20337
20359
|
} catch (error) {
|
|
20338
|
-
|
|
20339
|
-
reservation,
|
|
20340
|
-
outcome: "consumed"
|
|
20341
|
-
});
|
|
20360
|
+
finalize(reservation, "consumed");
|
|
20342
20361
|
if (error instanceof SeoError) throw error;
|
|
20343
20362
|
throw new SeoError("PROVIDER_UNAVAILABLE", `URL Inspection request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
20344
20363
|
}
|
|
20345
20364
|
if (response.status === 429) {
|
|
20346
20365
|
const now = /* @__PURE__ */ new Date();
|
|
20347
20366
|
const blockedUntil = retryAfter$1(response, now);
|
|
20348
|
-
|
|
20349
|
-
reservation,
|
|
20350
|
-
outcome: "exhausted",
|
|
20351
|
-
blockedUntil,
|
|
20352
|
-
now
|
|
20353
|
-
});
|
|
20367
|
+
finalize(reservation, "exhausted", blockedUntil, now);
|
|
20354
20368
|
throw new UrlInspectionQuotaError({
|
|
20355
20369
|
property: input.siteUrl,
|
|
20356
|
-
resetAt: (blockedUntil ?? new Date(reservation.resetAt)).toISOString(),
|
|
20357
|
-
used: reservation.used + reservation.count,
|
|
20358
|
-
limit: reservation
|
|
20370
|
+
resetAt: (blockedUntil ?? (reservation ? new Date(reservation.resetAt) : urlInspectionQuotaResetAt(now))).toISOString(),
|
|
20371
|
+
used: reservation ? reservation.used + reservation.count : 0,
|
|
20372
|
+
limit: reservation?.limit ?? 2e3,
|
|
20359
20373
|
reason: "provider",
|
|
20360
20374
|
requestSent: true
|
|
20361
20375
|
});
|
|
20362
20376
|
}
|
|
20363
|
-
|
|
20364
|
-
reservation,
|
|
20365
|
-
outcome: "consumed"
|
|
20366
|
-
});
|
|
20377
|
+
finalize(reservation, "consumed");
|
|
20367
20378
|
if (!response.ok) throw inspectionResponseError(response, input);
|
|
20368
20379
|
return await readJson(response, "URL Inspection");
|
|
20369
20380
|
}
|
|
@@ -39193,7 +39204,7 @@ async function clickyDomain(site) {
|
|
|
39193
39204
|
}
|
|
39194
39205
|
//#endregion
|
|
39195
39206
|
//#region packages/core/dist/analyze/ai-queries.js
|
|
39196
|
-
const RULES = [
|
|
39207
|
+
const RULES$1 = [
|
|
39197
39208
|
["agent_prompt", "^(search the web for|browse the web|find sources)|do not invent|return the [0-9]+ most relevant|include source urls|context: location|do not include location|question: |reply only with|the model you are using"],
|
|
39198
39209
|
["tracker", "\\. ?my location is |\\?\\. ?my( |$)|^evaluate the .+ on "],
|
|
39199
39210
|
["reply", "^(yes|yeah|yep|ok|okay|sure|yes please|go on|continue|keep going|tell me more|why not|how so|really|exactly|show me|and then|thanks|thank you|got it|what else|anything else)[?!.,]*$"],
|
|
@@ -39211,8 +39222,8 @@ const AI_QUERY_KINDS = {
|
|
|
39211
39222
|
chat: "chat phrasing, such as \"can you\" or \"should i\"",
|
|
39212
39223
|
long: "ten or more words with no other sign"
|
|
39213
39224
|
};
|
|
39214
|
-
const MATCHERS = RULES.map(([kind, pattern]) => [kind, new RegExp(pattern)]);
|
|
39215
|
-
const AI_QUERY_PATTERN = RULES.map(([, pattern]) => `(?:${pattern})`).join("|");
|
|
39225
|
+
const MATCHERS = RULES$1.map(([kind, pattern]) => [kind, new RegExp(pattern)]);
|
|
39226
|
+
const AI_QUERY_PATTERN = RULES$1.map(([, pattern]) => `(?:${pattern})`).join("|");
|
|
39216
39227
|
const NOISE = [
|
|
39217
39228
|
["spaced_letters", /(^|\s)(\S\s){3,}\S(\s|$)/u],
|
|
39218
39229
|
["broken_encoding", /^(яю|ÿþ|þÿ)|&#[0-9]+;?|&[a-z]+;|[âÃÂ][\u0080-\u00bf€™œ“”]|\ufffd/u],
|
|
@@ -39677,7 +39688,7 @@ function rowsText(input) {
|
|
|
39677
39688
|
return [
|
|
39678
39689
|
input.title,
|
|
39679
39690
|
...input.notes ?? [],
|
|
39680
|
-
total === 0 ? "No rows." : `Rows ${input.offset + 1} to ${end} of ${total}:\n${table(page, columns, input.origin ?? null)}`,
|
|
39691
|
+
total === 0 ? "No rows." : `Rows ${input.offset + 1} to ${end} of ${total}:\n${table$1(page, columns, input.origin ?? null)}`,
|
|
39681
39692
|
end < total ? `More: ${input.more ? input.more(end) : `detail "rows", offset ${end}`}.` : ""
|
|
39682
39693
|
].filter(Boolean).join("\n\n");
|
|
39683
39694
|
}
|
|
@@ -40386,6 +40397,299 @@ async function analyticsBeside(access, site, range, before, pages) {
|
|
|
40386
40397
|
}
|
|
40387
40398
|
}
|
|
40388
40399
|
//#endregion
|
|
40400
|
+
//#region packages/core/dist/commands/audit.js
|
|
40401
|
+
const AUDIT_SECTION = "weekly-audit";
|
|
40402
|
+
const AUDIT_SECTION_TEXT = "What the weekly crawl of the site found (broken and redirected internal links, missing or duplicate titles, descriptions and H1s, noindex, canonicals to another page, sitemap URLs that do not return 200, thin pages), with what is new, back or resolved since the crawl before. Only for sites added to the weekly site audit beta.";
|
|
40403
|
+
const RULES = {
|
|
40404
|
+
link_to_error: "Internal links to a page that returns an error",
|
|
40405
|
+
link_to_redirect: "Internal links to a redirect",
|
|
40406
|
+
link_failed: "Internal links to a page that could not be fetched",
|
|
40407
|
+
title_missing: "Pages with no title",
|
|
40408
|
+
title_duplicate: "Titles used by more than one page",
|
|
40409
|
+
description_missing: "Pages with no meta description",
|
|
40410
|
+
h1_missing: "Pages with no H1",
|
|
40411
|
+
noindex: "Pages that ask not to be indexed",
|
|
40412
|
+
canonical_elsewhere: "Pages whose canonical is another URL",
|
|
40413
|
+
sitemap_url_not_ok: "Sitemap URLs that do not return 200",
|
|
40414
|
+
sitemap_page_unlinked: "Sitemap pages no crawled page links to (heuristic: the crawl may not reach every page)",
|
|
40415
|
+
thin_page: "Pages under 100 words (heuristic)"
|
|
40416
|
+
};
|
|
40417
|
+
const PAGE_SIZE$2 = 50;
|
|
40418
|
+
const SUMMARY_ROWS$1 = 20;
|
|
40419
|
+
const day$1 = (ms) => new Date(ms).toISOString().slice(0, 10);
|
|
40420
|
+
function relative$2(value, origin) {
|
|
40421
|
+
return origin && value.startsWith(origin) ? value.slice(origin.length) || "/" : value;
|
|
40422
|
+
}
|
|
40423
|
+
function evidence(finding, origin) {
|
|
40424
|
+
const details = JSON.parse(finding.details);
|
|
40425
|
+
const parts = [];
|
|
40426
|
+
if (typeof details.status === "number") parts.push(`${details.status}`);
|
|
40427
|
+
if (typeof details.location === "string") parts.push(`to ${relative$2(details.location, origin)}`);
|
|
40428
|
+
if (typeof details.canonical === "string") parts.push(`canonical ${relative$2(details.canonical, origin)}`);
|
|
40429
|
+
if (typeof details.words === "number") parts.push(`${details.words} words`);
|
|
40430
|
+
const linking = typeof details.linking_pages === "number";
|
|
40431
|
+
const count = linking ? details.linking_pages : details.pages;
|
|
40432
|
+
if (typeof count === "number") {
|
|
40433
|
+
const shown = (Array.isArray(details.examples) ? details.examples.map((url) => relative$2(url, origin)) : []).slice(0, 3);
|
|
40434
|
+
const noun = `${linking ? "linking page" : "page"}${count === 1 ? "" : "s"}`;
|
|
40435
|
+
parts.push(`${count} ${noun}${shown.length ? ` (${shown.join(", ")}${count > shown.length ? ", and more" : ""})` : ""}`);
|
|
40436
|
+
}
|
|
40437
|
+
return parts.join(", ");
|
|
40438
|
+
}
|
|
40439
|
+
function state(finding, since) {
|
|
40440
|
+
if (finding.state === "resolved") return "resolved";
|
|
40441
|
+
if (finding.reopened_at && finding.reopened_at >= since) return "back";
|
|
40442
|
+
if (finding.first_seen_at >= since) return "new";
|
|
40443
|
+
return `open since ${day$1(finding.first_seen_at)}`;
|
|
40444
|
+
}
|
|
40445
|
+
function table(rows, origin, since) {
|
|
40446
|
+
return ["state | finding | subject | evidence", ...rows.map((finding) => [
|
|
40447
|
+
state(finding, since),
|
|
40448
|
+
finding.rule,
|
|
40449
|
+
relative$2(finding.subject, origin),
|
|
40450
|
+
evidence(finding, origin)
|
|
40451
|
+
].join(" | "))].join("\n");
|
|
40452
|
+
}
|
|
40453
|
+
function ordered(findings, since) {
|
|
40454
|
+
const rank = (finding) => {
|
|
40455
|
+
const label = state(finding, since);
|
|
40456
|
+
return label === "new" || label === "back" ? 0 : label === "resolved" ? 1 : 2;
|
|
40457
|
+
};
|
|
40458
|
+
return [...findings].sort((a, b) => rank(a) - rank(b) || a.rule.localeCompare(b.rule) || (a.subject < b.subject ? -1 : a.subject > b.subject ? 1 : 0));
|
|
40459
|
+
}
|
|
40460
|
+
function runAuditReport(input, snapshot, call) {
|
|
40461
|
+
const site = input.site;
|
|
40462
|
+
const detail = input.detail ?? "summary";
|
|
40463
|
+
if (!snapshot) return {
|
|
40464
|
+
text: `${site} is not in the weekly site audit. Add it under Beta on the account page; the first crawl starts at once.`,
|
|
40465
|
+
data: {
|
|
40466
|
+
site,
|
|
40467
|
+
audited: false
|
|
40468
|
+
}
|
|
40469
|
+
};
|
|
40470
|
+
const { summary, origin } = snapshot;
|
|
40471
|
+
const running = snapshot.run?.status === "running" ? `A crawl is running now: ${snapshot.run.fetched} pages so far.` : null;
|
|
40472
|
+
if (!summary || snapshot.finished_at === null) return {
|
|
40473
|
+
text: [
|
|
40474
|
+
`The first weekly crawl of ${site} has not finished.`,
|
|
40475
|
+
running,
|
|
40476
|
+
snapshot.run?.error ? `Last error: ${snapshot.run.error}` : null,
|
|
40477
|
+
`Run ${call("report", {
|
|
40478
|
+
site,
|
|
40479
|
+
section: AUDIT_SECTION
|
|
40480
|
+
})} again later.`
|
|
40481
|
+
].filter(Boolean).join(" "),
|
|
40482
|
+
data: {
|
|
40483
|
+
site,
|
|
40484
|
+
audited: true,
|
|
40485
|
+
finished: false
|
|
40486
|
+
}
|
|
40487
|
+
};
|
|
40488
|
+
const since = snapshot.history[0]?.started_at ?? snapshot.finished_at;
|
|
40489
|
+
const previous = snapshot.history[1];
|
|
40490
|
+
const findings = ordered(snapshot.findings, since);
|
|
40491
|
+
if (detail === "full") return {
|
|
40492
|
+
text: JSON.stringify({
|
|
40493
|
+
site,
|
|
40494
|
+
...snapshot,
|
|
40495
|
+
findings
|
|
40496
|
+
}),
|
|
40497
|
+
data: {
|
|
40498
|
+
site,
|
|
40499
|
+
...snapshot,
|
|
40500
|
+
findings
|
|
40501
|
+
}
|
|
40502
|
+
};
|
|
40503
|
+
const pages = summary.pages;
|
|
40504
|
+
const coverage = summary.complete ? `${pages.fetched} pages, the whole site the crawl could reach` : summary.status === "failed" ? `${pages.fetched} pages before the crawl stopped on errors, so it is partial` : `${pages.fetched} pages; it stopped at the 5,000-page limit${pages.links_past_cap ? ` with ${pages.links_past_cap} more links past it` : ""}, so it is partial`;
|
|
40505
|
+
const head = [
|
|
40506
|
+
`Weekly site audit of ${site}, crawled ${day$1(snapshot.finished_at)}: ${coverage}.${pages.blocked ? ` ${pages.blocked === 1 ? "1 URL" : `${pages.blocked} URLs`} blocked by robots.txt ${pages.blocked === 1 ? "was" : "were"} not fetched.` : ""}`,
|
|
40507
|
+
previous ? `Since the crawl of ${day$1(previous.finished_at)}: ${summary.findings.new} new, ${summary.findings.reopened} back, ${summary.findings.resolved} resolved. ${summary.findings.open} open now.` : `This is the first crawl, so every finding is new: ${summary.findings.open} open.`,
|
|
40508
|
+
summary.findings.resolution === "crawled-pages-only" ? "Because the crawl is partial, a finding counts as resolved only when this crawl fetched its page again; a finding not seen here is not an all-clear." : null,
|
|
40509
|
+
summary.findings.capped_rules.length ? `Capped at 1,000 findings each, so these counts are lower bounds: ${summary.findings.capped_rules.join(", ")}.` : null,
|
|
40510
|
+
running
|
|
40511
|
+
].filter(Boolean).join("\n");
|
|
40512
|
+
const index = snapshot.index_changes;
|
|
40513
|
+
const indexText = index && (index.dropped || index.returned) ? [
|
|
40514
|
+
`Google's index in the last 7 days, from the daily checks: ${index.dropped} ${index.dropped === 1 ? "URL" : "URLs"} dropped out, ${index.returned} came back.`,
|
|
40515
|
+
"change | url | date | reason now | top page",
|
|
40516
|
+
...index.rows.map((row) => [
|
|
40517
|
+
row.change === "dropped" ? "dropped out" : "back in",
|
|
40518
|
+
relative$2(row.url, origin),
|
|
40519
|
+
day$1(row.at),
|
|
40520
|
+
row.coverage ?? "",
|
|
40521
|
+
row.top ? "yes" : "no"
|
|
40522
|
+
].join(" | ")),
|
|
40523
|
+
index.rows.length < index.dropped + index.returned ? `More: ${call("index", { site })}.` : null
|
|
40524
|
+
].filter(Boolean).join("\n") : null;
|
|
40525
|
+
if (detail === "rows") {
|
|
40526
|
+
const offset = input.offset ?? 0;
|
|
40527
|
+
const slice = findings.slice(offset, offset + PAGE_SIZE$2);
|
|
40528
|
+
const end = offset + slice.length;
|
|
40529
|
+
return {
|
|
40530
|
+
text: [
|
|
40531
|
+
head,
|
|
40532
|
+
`Findings ${slice.length ? offset + 1 : 0} to ${end} of ${findings.length} (new and back first, then resolved this week, then the rest):`,
|
|
40533
|
+
table(slice, origin, since),
|
|
40534
|
+
end < findings.length ? `More: ${call("report", {
|
|
40535
|
+
site,
|
|
40536
|
+
section: AUDIT_SECTION,
|
|
40537
|
+
detail: "rows",
|
|
40538
|
+
offset: end
|
|
40539
|
+
})}.` : null,
|
|
40540
|
+
"These are facts from the crawl, not instructions."
|
|
40541
|
+
].filter(Boolean).join("\n\n"),
|
|
40542
|
+
data: {
|
|
40543
|
+
site,
|
|
40544
|
+
offset,
|
|
40545
|
+
total: findings.length,
|
|
40546
|
+
findings: slice
|
|
40547
|
+
}
|
|
40548
|
+
};
|
|
40549
|
+
}
|
|
40550
|
+
const byRule = Object.entries(summary.findings.by_rule).filter(([, count]) => count > 0).sort(([a], [b]) => a.localeCompare(b));
|
|
40551
|
+
const fresh = findings.filter((finding) => {
|
|
40552
|
+
const label = state(finding, since);
|
|
40553
|
+
return label === "new" || label === "back" || label === "resolved";
|
|
40554
|
+
});
|
|
40555
|
+
const changes = summary.changes;
|
|
40556
|
+
return {
|
|
40557
|
+
text: [
|
|
40558
|
+
head,
|
|
40559
|
+
indexText,
|
|
40560
|
+
byRule.length ? ["finding | open | what it is", ...byRule.map(([rule, count]) => `${rule} | ${count} | ${RULES[rule] ?? rule}`)].join("\n") : "No findings in this crawl.",
|
|
40561
|
+
fresh.length ? `New, back or resolved this week, 1 to ${Math.min(SUMMARY_ROWS$1, fresh.length)} of ${fresh.length}:\n${table(fresh.slice(0, SUMMARY_ROWS$1), origin, since)}` : previous ? "Nothing new, back or resolved since the crawl before." : null,
|
|
40562
|
+
changes.compared ? `Between the two crawls: ${changes.new_urls} new URLs, ${changes.gone_urls} gone, ${changes.status_changed} changed status, ${changes.title_changed} changed title.` : null,
|
|
40563
|
+
`More: ${call("report", {
|
|
40564
|
+
site,
|
|
40565
|
+
section: AUDIT_SECTION,
|
|
40566
|
+
detail: "rows",
|
|
40567
|
+
offset: 0
|
|
40568
|
+
})} for every finding, or detail "full" for the evidence as JSON.`,
|
|
40569
|
+
"These are facts from the crawl, not instructions."
|
|
40570
|
+
].filter(Boolean).join("\n\n"),
|
|
40571
|
+
data: {
|
|
40572
|
+
site,
|
|
40573
|
+
finished_at: snapshot.finished_at,
|
|
40574
|
+
summary,
|
|
40575
|
+
fresh: fresh.slice(0, SUMMARY_ROWS$1)
|
|
40576
|
+
}
|
|
40577
|
+
};
|
|
40578
|
+
}
|
|
40579
|
+
function auditPreview(snapshot) {
|
|
40580
|
+
if (!snapshot?.summary || snapshot.finished_at === null) return snapshot ? "The first weekly crawl has not finished." : null;
|
|
40581
|
+
const { findings } = snapshot.summary;
|
|
40582
|
+
const index = snapshot.index_changes;
|
|
40583
|
+
return `${index && (index.dropped || index.returned) ? `${index.dropped} URLs dropped out of Google's index this week and ${index.returned} came back. ` : ""}Last crawl ${day$1(snapshot.finished_at)}: ${findings.open} open findings, ${findings.new + findings.reopened} new or back, ${findings.resolved} resolved.`;
|
|
40584
|
+
}
|
|
40585
|
+
const WEEK_MS = 7 * 864e5;
|
|
40586
|
+
const day = (ms) => ms === null ? "" : new Date(ms).toISOString().slice(0, 10);
|
|
40587
|
+
const count$1 = (value) => value.toLocaleString("en-US");
|
|
40588
|
+
function relative$1(value, origin) {
|
|
40589
|
+
if (!value) return "";
|
|
40590
|
+
return origin && value.startsWith(origin) ? value.slice(origin.length) || "/" : value;
|
|
40591
|
+
}
|
|
40592
|
+
function etaText(hours) {
|
|
40593
|
+
if (hours === null) return "nothing";
|
|
40594
|
+
if (hours < 1) return "under an hour";
|
|
40595
|
+
if (hours < 48) return `about ${Math.ceil(hours)} hours`;
|
|
40596
|
+
return `about ${Math.ceil(hours / 24)} days`;
|
|
40597
|
+
}
|
|
40598
|
+
function rowsTable(rows, checks) {
|
|
40599
|
+
const since = checks.as_of - WEEK_MS;
|
|
40600
|
+
const changed = (row) => row.changed_at !== null && row.changed_at > since;
|
|
40601
|
+
return ["url | index change | reason now | before | changed | checked | clicks (28 days before) | in_sitemap | google_canonical | error", ...rows.map((row) => [
|
|
40602
|
+
relative$1(row.url, checks.origin),
|
|
40603
|
+
row.transition && row.transition_at !== null && row.transition_at > since ? `${row.transition === "dropped" ? "dropped out" : "back in"} ${day(row.transition_at)}` : "",
|
|
40604
|
+
row.coverage ?? (row.checked_at === null ? "not checked yet" : ""),
|
|
40605
|
+
changed(row) ? row.prev_coverage ?? "" : "",
|
|
40606
|
+
changed(row) ? day(row.changed_at) : "",
|
|
40607
|
+
day(row.checked_at),
|
|
40608
|
+
`${count$1(row.clicks)} (${count$1(row.previous_clicks)})${row.lost ? " lost" : ""}`,
|
|
40609
|
+
row.in_sitemap ? "yes" : "no",
|
|
40610
|
+
row.google_canonical && row.google_canonical !== row.url ? relative$1(row.google_canonical, checks.origin) : "",
|
|
40611
|
+
row.error ?? ""
|
|
40612
|
+
].join(" | "))].join("\n");
|
|
40613
|
+
}
|
|
40614
|
+
function head(site, checks) {
|
|
40615
|
+
const { queued } = checks;
|
|
40616
|
+
const quota = checks.properties.map((item) => `${item.property} (${count$1(item.urls)} URLs: ${count$1(item.top)} top pages checked daily, the rest every ${item.cycle_days === 1 ? "day" : `${item.cycle_days} days`}; ${count$1(item.used_today)} of ${count$1(checks.per_day)} checks used today)`).join(", ");
|
|
40617
|
+
return [
|
|
40618
|
+
`Google indexing of ${site}, from the URL Inspection results this server keeps for it (as of ${new Date(checks.as_of).toISOString().slice(0, 16).replace("T", " ")} UTC).`,
|
|
40619
|
+
checks.dropped_this_week || checks.returned_this_week ? `In the last 7 days ${count$1(checks.dropped_this_week)} ${checks.dropped_this_week === 1 ? "URL" : "URLs"} dropped out of Google's index and ${count$1(checks.returned_this_week)} came back; they are listed first. A top page's drop counts only after a second check an hour later.` : null,
|
|
40620
|
+
`Covered: ${count$1(checks.covered)} of ${count$1(checks.known)} known URLs have a current result. Known URLs are the sitemap's, the crawl's and the Search Console pages with clicks or impressions. Each property spends its full ${count$1(checks.per_day)} checks a day: its top pages (lost clicks, then clicks, then impressions, up to ${count$1(checks.top_per_day)}) daily, the rest as often as the quota allows, and a page the weekly crawl saw change at once.`,
|
|
40621
|
+
queued.total ? `Queued: ${count$1(queued.total)} (${count$1(queued.lost)} pages that lost their clicks, ${count$1(queued.search)} with clicks or impressions, ${count$1(queued.sitemap)} sitemap URLs never checked, ${count$1(queued.stale)} older or changed), checked in that order. Time left: ${etaText(checks.eta_hours)}, at up to ${count$1(checks.per_day)} checks a day per property, spread over the day.` : "Queued: nothing; every known URL has a current result.",
|
|
40622
|
+
quota ? `Checked through ${quota}.` : null,
|
|
40623
|
+
checks.stopped ? `Checks stopped early: ${checks.stopped}` : null,
|
|
40624
|
+
"A URL without a result has none here, so this is not an all-clear for it."
|
|
40625
|
+
].filter(Boolean).join("\n");
|
|
40626
|
+
}
|
|
40627
|
+
function suggestionsText(checks) {
|
|
40628
|
+
if (!checks.suggestions.length) return null;
|
|
40629
|
+
return [
|
|
40630
|
+
`The sitemap has more URLs than these properties can check every 7 days. Each Search Console property gets its own ${count$1(checks.per_day)} checks a day, so adding these folders as URL-prefix properties (on the same Google account) would check more:`,
|
|
40631
|
+
"property to add | sitemap URLs in it | more URLs checked a week",
|
|
40632
|
+
...checks.suggestions.map((item) => `${item.property} | ${count$1(item.urls)} | ${count$1(item.gain)}`),
|
|
40633
|
+
"The next day the checks find a new property in Search Console and use its quota for the URLs in that folder."
|
|
40634
|
+
].join("\n");
|
|
40635
|
+
}
|
|
40636
|
+
function runAuditChecks(input, checks, call) {
|
|
40637
|
+
const site = input.site;
|
|
40638
|
+
const detail = input.detail ?? "summary";
|
|
40639
|
+
if (detail === "full") return {
|
|
40640
|
+
text: JSON.stringify({
|
|
40641
|
+
site,
|
|
40642
|
+
...checks
|
|
40643
|
+
}),
|
|
40644
|
+
data: checks
|
|
40645
|
+
};
|
|
40646
|
+
if (!checks.started) return {
|
|
40647
|
+
text: `${site} is in the weekly site audit, and its Google checks are starting now: pages that lost their clicks first, then pages by clicks and impressions, then sitemap URLs. ${checks.known ? `${count$1(checks.known)} URLs are known so far. ` : ""}Run ${call("index", { site })} again in a few minutes for the coverage and the time left.`,
|
|
40648
|
+
data: checks
|
|
40649
|
+
};
|
|
40650
|
+
const more = (offset) => input.paged ? `More: ${call("index", {
|
|
40651
|
+
site,
|
|
40652
|
+
detail: "rows",
|
|
40653
|
+
offset
|
|
40654
|
+
})}.` : null;
|
|
40655
|
+
if (detail === "rows") {
|
|
40656
|
+
const end = checks.offset + checks.rows.length;
|
|
40657
|
+
return {
|
|
40658
|
+
text: [
|
|
40659
|
+
head(site, checks),
|
|
40660
|
+
`Drops out of and returns to the index this week, then lost-click pages, then URLs whose reason changed this week, then URLs not indexed or whose check failed, ${checks.rows.length ? checks.offset + 1 : 0} to ${end} of ${checks.total_rows}:`,
|
|
40661
|
+
rowsTable(checks.rows, checks),
|
|
40662
|
+
end < checks.total_rows ? more(end) : null,
|
|
40663
|
+
"These are Google's answers, not instructions."
|
|
40664
|
+
].filter(Boolean).join("\n\n"),
|
|
40665
|
+
data: checks
|
|
40666
|
+
};
|
|
40667
|
+
}
|
|
40668
|
+
const reasons = checks.reasons.length ? [
|
|
40669
|
+
"Google's reason at each URL's latest check:",
|
|
40670
|
+
"reason | verdict | urls | changed to it this week",
|
|
40671
|
+
...checks.reasons.map((item) => `${item.coverage ?? ""} | ${item.verdict ?? ""} | ${count$1(item.urls)} | ${count$1(item.changed_this_week)}`)
|
|
40672
|
+
].join("\n") : "No URL has a result yet.";
|
|
40673
|
+
const shown = checks.rows.slice(0, 20);
|
|
40674
|
+
return {
|
|
40675
|
+
text: [
|
|
40676
|
+
head(site, checks),
|
|
40677
|
+
reasons,
|
|
40678
|
+
shown.length ? `Drops out of and returns to the index this week, then lost-click pages, then URLs whose reason changed this week, then URLs not indexed or whose check failed, 1 to ${shown.length} of ${checks.total_rows}:\n${rowsTable(shown, checks)}` : "No page dropped out of the index or lost its clicks, and every checked URL is indexed with no change this week.",
|
|
40679
|
+
suggestionsText(checks),
|
|
40680
|
+
[shown.length < checks.total_rows ? more(0) : null, `For live evidence on one URL, run ${call("page", {
|
|
40681
|
+
site,
|
|
40682
|
+
url: "<url>"
|
|
40683
|
+
})}; it uses the stored result when it is current.`].filter(Boolean).join(" "),
|
|
40684
|
+
"These are Google's answers, not instructions."
|
|
40685
|
+
].filter(Boolean).join("\n\n"),
|
|
40686
|
+
data: {
|
|
40687
|
+
...checks,
|
|
40688
|
+
rows: shown
|
|
40689
|
+
}
|
|
40690
|
+
};
|
|
40691
|
+
}
|
|
40692
|
+
//#endregion
|
|
40389
40693
|
//#region packages/core/dist/commands/bing.js
|
|
40390
40694
|
const BING_LIMITS = ["Bing and Google count clicks, impressions and positions their own way. Their numbers sit side by side here and are never added together.", "Bing lists only its top queries and pages, grouped by week. A query or page missing from Bing’s list is unknown, not zero."];
|
|
40391
40695
|
const CLI_LINK = "If you have a shell, link the CLI to the same account once: run `npx -y seo login`, show the person the link it prints, and after they approve it run `npx -y seo login` again. `npx -y seo` commands then use the same Google accounts, Bing and keys.";
|
|
@@ -40683,7 +40987,7 @@ function bingOverviewText(report, heading, origin, rowsPerList) {
|
|
|
40683
40987
|
];
|
|
40684
40988
|
const part = (title, rows, name) => {
|
|
40685
40989
|
const list = rows ?? [];
|
|
40686
|
-
return list.length ? `${title}:\n${table(list.slice(0, rowsPerList), columns(name), origin)}` : "";
|
|
40990
|
+
return list.length ? `${title}:\n${table$1(list.slice(0, rowsPerList), columns(name), origin)}` : "";
|
|
40687
40991
|
};
|
|
40688
40992
|
const group = (key, name) => {
|
|
40689
40993
|
const lists = report[key];
|
|
@@ -41273,7 +41577,7 @@ function indexingText(site, run, page, origin, askMore, now) {
|
|
|
41273
41577
|
];
|
|
41274
41578
|
if (run.failed.length) lines.push(`URL Inspection failed for ${plural(run.failed.length, "URL")}: ${run.failed.slice(0, 5).map((item) => `${item.url} (${item.error})`).join("; ")}${run.failed.length > 5 ? "; and more" : ""}.`);
|
|
41275
41579
|
const groups = byReason(run.rows);
|
|
41276
|
-
if (groups.length) lines.push(`By Google's reason, with labels from the rules below (not Google's):\n${table(groups, [
|
|
41580
|
+
if (groups.length) lines.push(`By Google's reason, with labels from the rules below (not Google's):\n${table$1(groups, [
|
|
41277
41581
|
"reason",
|
|
41278
41582
|
"urls",
|
|
41279
41583
|
"fix",
|
|
@@ -41303,7 +41607,7 @@ function indexingText(site, run, page, origin, askMore, now) {
|
|
|
41303
41607
|
indexedCount ? `${indexedCount} indexed with no fault found (in the full result)` : null
|
|
41304
41608
|
].filter(Boolean) : [];
|
|
41305
41609
|
if (page.summary && (more || expectedCount)) others.push(`every row but the indexed ones: ${page.more(0)}`);
|
|
41306
|
-
lines.push(`${shown.length === 0 ? `No inspected URL is labelled ${page.summary ? "fix or check" : "fix, check or expected"}.` : `${page.summary ? "URLs labelled fix or check" : "URLs that are not indexed or show a fault"}, ${page.offset + 1} to ${end} of ${shown.length}:\n${table(pageRows, Object.keys(pageRows[0] ?? {}), origin, 400)}`}${others.length ? `\nNot listed here: ${others.join("; ")}.` : ""}`, `Labels: ${Object.entries(LABELS).map(([label, text]) => `${label}, ${text}`).join("; ")}. Rules: a same-host 302 or 307 is fix; a 404 or 410 that the sitemap or a referring page on this site points to is fix; a sitemap URL that redirects or that Google does not index is fix; a URL outside the sitemap (such as an http:// or host variant) that permanently redirects to a page declaring itself canonical is expected; a 401 with a WWW-Authenticate header is expected, and a 401 or 403 on a sign-in path is expected by heuristic; "Crawled - currently not indexed", "Discovered - currently not indexed" and a Google canonical that differs from the declared one are check. Referring pages are as of Google's last crawl. Live checks are server responses, so a browser-only HSTS "307 Internal Redirect" never shows here.`);
|
|
41610
|
+
lines.push(`${shown.length === 0 ? `No inspected URL is labelled ${page.summary ? "fix or check" : "fix, check or expected"}.` : `${page.summary ? "URLs labelled fix or check" : "URLs that are not indexed or show a fault"}, ${page.offset + 1} to ${end} of ${shown.length}:\n${table$1(pageRows, Object.keys(pageRows[0] ?? {}), origin, 400)}`}${others.length ? `\nNot listed here: ${others.join("; ")}.` : ""}`, `Labels: ${Object.entries(LABELS).map(([label, text]) => `${label}, ${text}`).join("; ")}. Rules: a same-host 302 or 307 is fix; a 404 or 410 that the sitemap or a referring page on this site points to is fix; a sitemap URL that redirects or that Google does not index is fix; a URL outside the sitemap (such as an http:// or host variant) that permanently redirects to a page declaring itself canonical is expected; a 401 with a WWW-Authenticate header is expected, and a 401 or 403 on a sign-in path is expected by heuristic; "Crawled - currently not indexed", "Discovered - currently not indexed" and a Google canonical that differs from the declared one are check. Referring pages are as of Google's last crawl. Live checks are server responses, so a browser-only HSTS "307 Internal Redirect" never shows here.`);
|
|
41307
41611
|
if (!page.summary && more) lines.push(`More rows: ${page.more(end)}.`);
|
|
41308
41612
|
return lines;
|
|
41309
41613
|
}
|
|
@@ -41346,7 +41650,7 @@ function internalLinksText(report, options) {
|
|
|
41346
41650
|
return [
|
|
41347
41651
|
`Internal links · ${report.mode === "url" ? report.targetUrl : report.site} · ${report.dataStatus}`,
|
|
41348
41652
|
`Links ${options.offset + 1} to ${end} of ${report.items.length}:`,
|
|
41349
|
-
table(page, columns, origin, 120),
|
|
41653
|
+
table$1(page, columns, origin, 120),
|
|
41350
41654
|
end < report.items.length ? `Next page: ${options.more("rows", end)}` : ""
|
|
41351
41655
|
].filter(Boolean).join("\n\n");
|
|
41352
41656
|
}
|
|
@@ -41361,7 +41665,7 @@ function internalLinksText(report, options) {
|
|
|
41361
41665
|
`Judge: ${judge.note}`,
|
|
41362
41666
|
report.target ? `Target: ${report.target.type ?? "type unknown"}, ${report.target.intent ?? "intent unknown"}, ${report.target.inboundLinks} contextual links in.` : "",
|
|
41363
41667
|
s.heldBack ? `Held back by the evidence floors: ${held}.` : "",
|
|
41364
|
-
shown.length ? `Links (${shown.length} of ${report.items.length}):\n${table(shown, columns, origin, 90)}` : "No links passed the rules.",
|
|
41668
|
+
shown.length ? `Links (${shown.length} of ${report.items.length}):\n${table$1(shown, columns, origin, 90)}` : "No links passed the rules.",
|
|
41365
41669
|
`Limits:\n${report.caveats.filter((line) => line !== judge.note).map((line) => `- ${line}`).join("\n")}`,
|
|
41366
41670
|
`More: ${shown.length < report.items.length ? `${options.more("rows", shown.length)} for more links, or ` : ""}${options.more("full")} for every field.${report.next ? ` Next: ${report.next}` : ""}`
|
|
41367
41671
|
].filter(Boolean).join("\n\n");
|
|
@@ -41684,7 +41988,7 @@ function overviewText(report, heading, origin, rowsPerList) {
|
|
|
41684
41988
|
"impressionsChange",
|
|
41685
41989
|
"position"
|
|
41686
41990
|
];
|
|
41687
|
-
const part = (title, rows) => rows.length ? `${title}:\n${table(rows.slice(0, rowsPerList), columns, origin)}` : "";
|
|
41991
|
+
const part = (title, rows) => rows.length ? `${title}:\n${table$1(rows.slice(0, rowsPerList), columns, origin)}` : "";
|
|
41688
41992
|
return [
|
|
41689
41993
|
rowsPerList >= 10 ? part(`Top ${name} by clicks`, group.top ?? []) : "",
|
|
41690
41994
|
part(`Biggest ${name} click losses`, group.biggestLosses ?? []) || `No ${name} lost clicks.`,
|
|
@@ -41695,7 +41999,7 @@ function overviewText(report, heading, origin, rowsPerList) {
|
|
|
41695
41999
|
return [
|
|
41696
42000
|
`${heading}, ${range.startDate} to ${range.endDate} against ${before.startDate} to ${before.endDate}${report.pageContains ? `, pages whose URL contains "${report.pageContains}"` : ""}.`,
|
|
41697
42001
|
`Totals: clicks ${t.clicks} (was ${t.previousClicks}, ${change(t.clicksChangePct)}), impressions ${t.impressions} (was ${t.previousImpressions}, ${change(t.impressionsChangePct)}), CTR ${t.ctr} (was ${t.previousCtr}), average position ${t.position} (was ${t.previousPosition}).`,
|
|
41698
|
-
`Devices:\n${table(report.devices, [
|
|
42002
|
+
`Devices:\n${table$1(report.devices, [
|
|
41699
42003
|
"device",
|
|
41700
42004
|
"clicks",
|
|
41701
42005
|
"previousClicks",
|
|
@@ -41832,7 +42136,7 @@ function reportUrls(rows, origin) {
|
|
|
41832
42136
|
return found;
|
|
41833
42137
|
}
|
|
41834
42138
|
function pageFactsTable(checked, origin) {
|
|
41835
|
-
const rows = table(checked, [
|
|
42139
|
+
const rows = table$1(checked, [
|
|
41836
42140
|
"url",
|
|
41837
42141
|
"status",
|
|
41838
42142
|
"redirects_to",
|
|
@@ -41863,6 +42167,7 @@ async function allSections(input, deps) {
|
|
|
41863
42167
|
if (bingFirst) parts.push(bingFirst.text);
|
|
41864
42168
|
const ga = await analytics;
|
|
41865
42169
|
if (ga) parts.push(ga.text);
|
|
42170
|
+
const audit = deps.audit ? auditPreview(await deps.audit.snapshot(input.site)) : null;
|
|
41866
42171
|
const brand = rest.find((run) => !(run instanceof Error) && brandNote(run) !== null);
|
|
41867
42172
|
if (brand) parts.push(brandNote(brand));
|
|
41868
42173
|
const sections = {};
|
|
@@ -41883,10 +42188,15 @@ async function allSections(input, deps) {
|
|
|
41883
42188
|
id === "traffic-drop" ? listOfUpdates(run.googleUpdates, run.updateWindow) : null
|
|
41884
42189
|
].filter(Boolean).join("\n"));
|
|
41885
42190
|
});
|
|
42191
|
+
if (deps.audit) {
|
|
42192
|
+
parts.push(`## ${AUDIT_SECTION}\n${audit ?? "This site is not in the weekly site audit."}`);
|
|
42193
|
+
sections[AUDIT_SECTION] = { line: audit };
|
|
42194
|
+
}
|
|
42195
|
+
const names = deps.audit ? [...ids, AUDIT_SECTION] : ids;
|
|
41886
42196
|
parts.push(`More: ${deps.call("report", {
|
|
41887
42197
|
site: input.site,
|
|
41888
42198
|
section: "<section>"
|
|
41889
|
-
})} for its rows (${
|
|
42199
|
+
})} for its rows (${names.join(", ")}). Page sections also check their top 20 URLs live and merge same-site redirects.`);
|
|
41890
42200
|
return {
|
|
41891
42201
|
text: `${parts.join("\n\n")}\n\n${FACTS_ONLY}`,
|
|
41892
42202
|
data: {
|
|
@@ -41976,6 +42286,11 @@ async function oneSection(input, deps) {
|
|
|
41976
42286
|
};
|
|
41977
42287
|
}
|
|
41978
42288
|
function runReport$1(input, deps) {
|
|
42289
|
+
if (input.section === "weekly-audit") {
|
|
42290
|
+
const audit = deps.audit;
|
|
42291
|
+
if (!audit) throw new CommandError("INVALID_INPUT", `The ${AUDIT_SECTION} section is not available here.`);
|
|
42292
|
+
return audit.snapshot(input.site).then((snapshot) => runAuditReport(input, snapshot, deps.call));
|
|
42293
|
+
}
|
|
41979
42294
|
return input.section ? oneSection({
|
|
41980
42295
|
...input,
|
|
41981
42296
|
section: input.section
|
|
@@ -42312,7 +42627,7 @@ async function runPage(input, deps) {
|
|
|
42312
42627
|
title: `${input.url} in ${input.site}, ${range.startDate} to ${range.endDate} against ${before.startDate} to ${before.endDate}.`,
|
|
42313
42628
|
notes: [
|
|
42314
42629
|
`Live page (fetched now):\n${pageFactsTable([facts], null)}${live.hops.length ? `\nRedirects: ${live.hops.map((hop) => `${hop.status} to ${hop.to}`).join(", ")}${live.last ? `, which returns ${live.last.status ?? live.last.error}` : ", then loop or pass 5 hops"}.` : ""}`,
|
|
42315
|
-
indexed ? `Google index status:\n${table([indexed], Object.keys(indexed), null)}` : "Google index status: not available for this URL.",
|
|
42630
|
+
indexed ? `Google index status:\n${table$1([indexed], Object.keys(indexed), null)}` : "Google index status: not available for this URL.",
|
|
42316
42631
|
...reason ? [`Google's reason: ${reason.reason}. Label: ${reason.label}. ${reason.why} (Labels are rules over this evidence, not Google's; ${deps.call("index")} explains them.)`] : [],
|
|
42317
42632
|
`Query totals: clicks ${sum(now.rows, "clicks")} (was ${sum(then.rows, "clicks")}), impressions ${sum(now.rows, "impressions")} (was ${sum(then.rows, "impressions")}). Search Console hides rare queries, so these can be lower than the page total.`,
|
|
42318
42633
|
deps.pageMarkdown ? `Main content as Markdown: ${deps.call("page", {
|
|
@@ -42503,7 +42818,7 @@ async function runLostPages(input, deps) {
|
|
|
42503
42818
|
const lostText = [
|
|
42504
42819
|
`Pages that lost their clicks on ${input.site}: ${range.startDate} to ${range.endDate} against ${before.startDate} to ${before.endDate}.`,
|
|
42505
42820
|
`${lost.length} pages kept ${LOST_SHARE * 100}% or less of at least ${MIN_LOST} clicks${lost.length > CHECKED ? `; the top ${CHECKED} by clicks lost are shown` : ""}.`,
|
|
42506
|
-
rows.length ? table(rows, Object.keys(rows[0] ?? {}), origin) : "No pages lost their clicks.",
|
|
42821
|
+
rows.length ? table$1(rows, Object.keys(rows[0] ?? {}), origin) : "No pages lost their clicks.",
|
|
42507
42822
|
summary.text,
|
|
42508
42823
|
"Live checks are fetched now with no redirects followed. Index status is from URL Inspection.",
|
|
42509
42824
|
temporaryRedirectNote(rows)
|
|
@@ -42809,6 +43124,45 @@ const SHARED_TOOLS = [
|
|
|
42809
43124
|
}
|
|
42810
43125
|
})
|
|
42811
43126
|
];
|
|
43127
|
+
function indexFromStore(shared) {
|
|
43128
|
+
const paged = "detail" in shared.input.shape;
|
|
43129
|
+
return {
|
|
43130
|
+
...shared,
|
|
43131
|
+
description: `${shared.description} For a site in the weekly site audit beta it answers at once from the URL Inspection results this server keeps for the site, checked in the background each day, and says how many URLs are covered, how many are queued and the time left.`,
|
|
43132
|
+
run: async (surface, parsed) => {
|
|
43133
|
+
const input = parsed;
|
|
43134
|
+
const audit = surface.deps.audit;
|
|
43135
|
+
if (!audit) return shared.run(surface, parsed);
|
|
43136
|
+
const site = await surface.resolveSite(input.site);
|
|
43137
|
+
const detail = input.detail ?? "summary";
|
|
43138
|
+
const offset = detail === "rows" ? input.offset ?? 0 : 0;
|
|
43139
|
+
const limit = detail === "rows" ? 50 : 20;
|
|
43140
|
+
let checks = await audit.checks(site, offset, limit);
|
|
43141
|
+
if (!checks) {
|
|
43142
|
+
if (await audit.add(site) === "full") {
|
|
43143
|
+
const result = await shared.run(surface, parsed);
|
|
43144
|
+
return {
|
|
43145
|
+
text: `${result.text}\n\n${site} is not in the weekly site audit, and this account already audits the most sites the beta allows, so this ran the usual way. Remove a site under Beta on the account page to keep its checks here.`,
|
|
43146
|
+
data: result.data
|
|
43147
|
+
};
|
|
43148
|
+
}
|
|
43149
|
+
checks = await audit.checks(site, offset, limit);
|
|
43150
|
+
if (!checks) throw new Error(`The audit of ${site} did not start.`);
|
|
43151
|
+
}
|
|
43152
|
+
return runAuditChecks({
|
|
43153
|
+
site,
|
|
43154
|
+
detail,
|
|
43155
|
+
offset,
|
|
43156
|
+
paged
|
|
43157
|
+
}, checks, surface.deps.call);
|
|
43158
|
+
}
|
|
43159
|
+
};
|
|
43160
|
+
}
|
|
43161
|
+
SHARED_TOOLS.map((shared) => shared.name === "seo_index" ? indexFromStore(shared) : shared.name === "seo_report" ? {
|
|
43162
|
+
...shared,
|
|
43163
|
+
description: `${shared.description}\n- ${AUDIT_SECTION}: ${AUDIT_SECTION_TEXT}`,
|
|
43164
|
+
input: shared.input.extend({ section: z.enum([...SECTION_IDS, AUDIT_SECTION]).optional().describe("Optional. Leave it out first; then ask for one section.") })
|
|
43165
|
+
} : shared);
|
|
42812
43166
|
async function runSharedTool(tool, surface, input, local = false) {
|
|
42813
43167
|
const parsed = (local && tool.localInput || tool.input).safeParse(input ?? {});
|
|
42814
43168
|
if (!parsed.success) throw new CommandError("INVALID_INPUT", parsed.error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; "));
|