@reddoorla/maintenance 0.91.0 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bump-deps-KGH5UYSF.js → bump-deps-GI7PHMOC.js} +6 -6
- package/dist/{chunk-A6T2R63Z.js → chunk-3HS6LPFX.js} +2 -2
- package/dist/{chunk-SJOXWJ5Y.js → chunk-4763KQ6O.js} +184 -9
- package/dist/chunk-4763KQ6O.js.map +1 -0
- package/dist/{chunk-UXJZU23T.js → chunk-4NUKMPOO.js} +9 -9
- package/dist/{chunk-BQRFQSGY.js → chunk-4R3YAQNV.js} +2 -2
- package/dist/{chunk-XIDUYLSR.js → chunk-7CJ3DVZW.js} +2 -2
- package/dist/{chunk-Z3KYFUN6.js → chunk-B6NUZ2ZX.js} +2 -2
- package/dist/{chunk-WVVT6SJK.js → chunk-BJQCIJOY.js} +169 -17
- package/dist/chunk-BJQCIJOY.js.map +1 -0
- package/dist/{chunk-Y53WOQIJ.js → chunk-CVJF2IC5.js} +2 -2
- package/dist/chunk-I7NHLVVI.js +89 -0
- package/dist/chunk-I7NHLVVI.js.map +1 -0
- package/dist/{chunk-QD427NIO.js → chunk-JB4Q3C3M.js} +7 -7
- package/dist/{chunk-IYSEC543.js → chunk-LS6QPANO.js} +2 -2
- package/dist/{chunk-7RJ2HMMJ.js → chunk-NTLSMJHW.js} +2 -2
- package/dist/{chunk-2NJQLSEU.js → chunk-PPIZHDJL.js} +2 -2
- package/dist/{chunk-JRHIFA5R.js → chunk-QF6IQQFP.js} +10 -4
- package/dist/chunk-QF6IQQFP.js.map +1 -0
- package/dist/{chunk-RQLXEMEE.js → chunk-T7ZUUOZ2.js} +2 -2
- package/dist/chunk-T7ZUUOZ2.js.map +1 -0
- package/dist/{chunk-TVL5MVUF.js → chunk-TCF6S7AD.js} +4 -4
- package/dist/{chunk-GSWTT2J6.js → chunk-WHWVYL7B.js} +4 -4
- package/dist/{chunk-3G25KIWW.js → chunk-ZTPPCHDB.js} +4 -4
- package/dist/cli/bin.js +17 -17
- package/dist/cli/commands/audit.js +10 -10
- package/dist/{convert-to-pnpm-NWB5FCQ2.js → convert-to-pnpm-ONFP3JKF.js} +6 -6
- package/dist/{db-IMMQSRAB.js → db-HSZ6D5DU.js} +3 -3
- package/dist/digest-EOA4JLTV.js +1 -1
- package/dist/digest-collectors-ZA3BTS3R.js +1 -1
- package/dist/{email-PLPDJUDH.js → email-I4BCETI4.js} +16 -65
- package/dist/email-I4BCETI4.js.map +1 -0
- package/dist/forms/index.d.ts +5 -1
- package/dist/forms/index.js +1 -1
- package/dist/forms/index.js.map +1 -1
- package/dist/forms/prismic.d.ts +92 -0
- package/dist/forms/prismic.js +105 -0
- package/dist/forms/prismic.js.map +1 -0
- package/dist/{forms-notify-target-YCZUAHTY.js → forms-notify-target-RMZPKT6S.js} +4 -3
- package/dist/{forms-notify-target-YCZUAHTY.js.map → forms-notify-target-RMZPKT6S.js.map} +1 -1
- package/dist/{health-endpoint-UVX6R5N5.js → health-endpoint-DHKJUPRM.js} +6 -6
- package/dist/index.js +30 -23
- package/dist/index.js.map +1 -1
- package/dist/{init-NXZ4NXTL.js → init-ZRKWYCKA.js} +15 -15
- package/dist/{launch-WPEQ3TY2.js → launch-6X5OXPF2.js} +11 -11
- package/dist/{notify-4HP4GZ2H.js → notify-IISTHNBR.js} +4 -3
- package/dist/{onboard-BKCYBAVW.js → onboard-6NHGCKGB.js} +6 -6
- package/dist/orchestrate-KMQIUP3H.js +1 -1
- package/dist/{pipeline-HQJYZ443.js → pipeline-AQBFG5WM.js} +5 -3
- package/dist/preflight-VPHQYWTY.js +1 -1
- package/dist/{prismic-ci-6YGRIOHT.js → prismic-ci-A2HI7NNJ.js} +8 -8
- package/dist/{prismic-models-FPDSUXDO.js → prismic-models-QTKIMFBC.js} +4 -4
- package/dist/prospect/types.d.ts +17 -0
- package/dist/prospect/types.js +1 -1
- package/dist/{prospect-audit-TFIXSTE5.js → prospect-audit-RKUVSTLN.js} +4 -4
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/{chunk-H6BLTEFN.js → render-7G43BSK5.js} +3 -3
- package/dist/{chunk-H6BLTEFN.js.map → render-7G43BSK5.js.map} +1 -1
- package/dist/{replay-4S6ENYCH.js → replay-PCSQOGCZ.js} +3 -2
- package/dist/{replay-4S6ENYCH.js.map → replay-PCSQOGCZ.js.map} +1 -1
- package/dist/{self-updating-VQIW24RZ.js → self-updating-D5XOWS7V.js} +5 -5
- package/dist/selftest-IFGSJP4X.js +1 -1
- package/dist/{smoke-suite-4JFZZYVW.js → smoke-suite-GXC5JZPH.js} +6 -6
- package/dist/{svelte-codemods-QAPDZC6F.js → svelte-codemods-A3EARSRP.js} +6 -6
- package/dist/{sync-configs-PVRLZRKL.js → sync-configs-KSESOHFF.js} +6 -6
- package/dist/{upgrade-IR2OO3KP.js → upgrade-ZLNVEIEA.js} +6 -6
- package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
- package/package.json +5 -1
- package/dist/chunk-JRHIFA5R.js.map +0 -1
- package/dist/chunk-RQLXEMEE.js.map +0 -1
- package/dist/chunk-SJOXWJ5Y.js.map +0 -1
- package/dist/chunk-WVVT6SJK.js.map +0 -1
- package/dist/email-PLPDJUDH.js.map +0 -1
- package/dist/render-3GYW2J34.js +0 -11
- package/dist/render-3GYW2J34.js.map +0 -1
- /package/dist/{bump-deps-KGH5UYSF.js.map → bump-deps-GI7PHMOC.js.map} +0 -0
- /package/dist/{chunk-A6T2R63Z.js.map → chunk-3HS6LPFX.js.map} +0 -0
- /package/dist/{chunk-UXJZU23T.js.map → chunk-4NUKMPOO.js.map} +0 -0
- /package/dist/{chunk-BQRFQSGY.js.map → chunk-4R3YAQNV.js.map} +0 -0
- /package/dist/{chunk-XIDUYLSR.js.map → chunk-7CJ3DVZW.js.map} +0 -0
- /package/dist/{chunk-Z3KYFUN6.js.map → chunk-B6NUZ2ZX.js.map} +0 -0
- /package/dist/{chunk-Y53WOQIJ.js.map → chunk-CVJF2IC5.js.map} +0 -0
- /package/dist/{chunk-QD427NIO.js.map → chunk-JB4Q3C3M.js.map} +0 -0
- /package/dist/{chunk-IYSEC543.js.map → chunk-LS6QPANO.js.map} +0 -0
- /package/dist/{chunk-7RJ2HMMJ.js.map → chunk-NTLSMJHW.js.map} +0 -0
- /package/dist/{chunk-2NJQLSEU.js.map → chunk-PPIZHDJL.js.map} +0 -0
- /package/dist/{chunk-TVL5MVUF.js.map → chunk-TCF6S7AD.js.map} +0 -0
- /package/dist/{chunk-GSWTT2J6.js.map → chunk-WHWVYL7B.js.map} +0 -0
- /package/dist/{chunk-3G25KIWW.js.map → chunk-ZTPPCHDB.js.map} +0 -0
- /package/dist/{convert-to-pnpm-NWB5FCQ2.js.map → convert-to-pnpm-ONFP3JKF.js.map} +0 -0
- /package/dist/{db-IMMQSRAB.js.map → db-HSZ6D5DU.js.map} +0 -0
- /package/dist/{health-endpoint-UVX6R5N5.js.map → health-endpoint-DHKJUPRM.js.map} +0 -0
- /package/dist/{init-NXZ4NXTL.js.map → init-ZRKWYCKA.js.map} +0 -0
- /package/dist/{launch-WPEQ3TY2.js.map → launch-6X5OXPF2.js.map} +0 -0
- /package/dist/{notify-4HP4GZ2H.js.map → notify-IISTHNBR.js.map} +0 -0
- /package/dist/{onboard-BKCYBAVW.js.map → onboard-6NHGCKGB.js.map} +0 -0
- /package/dist/{pipeline-HQJYZ443.js.map → pipeline-AQBFG5WM.js.map} +0 -0
- /package/dist/{prismic-ci-6YGRIOHT.js.map → prismic-ci-A2HI7NNJ.js.map} +0 -0
- /package/dist/{prismic-models-FPDSUXDO.js.map → prismic-models-QTKIMFBC.js.map} +0 -0
- /package/dist/{prospect-audit-TFIXSTE5.js.map → prospect-audit-RKUVSTLN.js.map} +0 -0
- /package/dist/{self-updating-VQIW24RZ.js.map → self-updating-D5XOWS7V.js.map} +0 -0
- /package/dist/{smoke-suite-4JFZZYVW.js.map → smoke-suite-GXC5JZPH.js.map} +0 -0
- /package/dist/{svelte-codemods-QAPDZC6F.js.map → svelte-codemods-A3EARSRP.js.map} +0 -0
- /package/dist/{sync-configs-PVRLZRKL.js.map → sync-configs-KSESOHFF.js.map} +0 -0
- /package/dist/{upgrade-IR2OO3KP.js.map → upgrade-ZLNVEIEA.js.map} +0 -0
- /package/dist/{webflow-624FEBFD.js.map → webflow-IRKOIT7S.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
checkGoal
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-T7ZUUOZ2.js";
|
|
4
4
|
import {
|
|
5
5
|
isPrivateOrLoopbackHost
|
|
6
6
|
} from "./chunk-RARREDQE.js";
|
|
@@ -1273,6 +1273,10 @@ Return:
|
|
|
1273
1273
|
at it with a pronoun has no antecedent and the engine will answer that it does not know who is meant.
|
|
1274
1274
|
These are searches, not conversational questions, and they are not the buyerQuestions above.
|
|
1275
1275
|
- fixes: prioritized, concrete, specific to this site. No generic SEO advice.
|
|
1276
|
+
Each "why" describes what a buyer or a crawler can or cannot read on the site TODAY. Never predict
|
|
1277
|
+
what an answer engine will cite, repeat, rank or recommend: the report states elsewhere, with
|
|
1278
|
+
evidence, that nothing on the site reliably moves that, and a fix that promises it contradicts the
|
|
1279
|
+
report. "A buyer cannot find a price" is a reason; "an engine will have nothing to cite" is not.
|
|
1276
1280
|
Set addresses to the key of the measured requirement a fix would satisfy \u2014 one of the keys listed
|
|
1277
1281
|
under "What we have already measured", exactly as written \u2014 or null when it answers to none of them.
|
|
1278
1282
|
Null is the normal case: a heavy image, a broken link or a stale copyright year maps to no
|
|
@@ -1462,7 +1466,12 @@ async function analyzeSite(url, crawl, checks, deps = defaultAnalyzeDeps(), goal
|
|
|
1462
1466
|
...parsed,
|
|
1463
1467
|
questionSetId: set.id,
|
|
1464
1468
|
buyerQuestions: conformToSet(parsed.buyerQuestions, set),
|
|
1465
|
-
fixes
|
|
1469
|
+
// Stamped here, at the one place model fixes enter the system, so the
|
|
1470
|
+
// renderer can label them as judgement and never as a finding.
|
|
1471
|
+
fixes: reconcileFixes(parsed.fixes, goalFit).map((f) => ({
|
|
1472
|
+
...f,
|
|
1473
|
+
origin: "recommendation"
|
|
1474
|
+
}))
|
|
1466
1475
|
};
|
|
1467
1476
|
return verifyEvidence(conformed, crawl);
|
|
1468
1477
|
}
|
|
@@ -2050,6 +2059,7 @@ async function classifyDomains(prospectUrl, prospectPhones, domains, deps) {
|
|
|
2050
2059
|
var MAX_PAGES2 = 14;
|
|
2051
2060
|
var MAX_TOTAL_CHARS = 12e4;
|
|
2052
2061
|
var MAX_ASSERTIONS = 12;
|
|
2062
|
+
var NO_CONFLATION = { detected: false, otherNames: [], engineQuote: null };
|
|
2053
2063
|
var AccuracySchema = z2.object({
|
|
2054
2064
|
assertions: z2.array(
|
|
2055
2065
|
z2.object({
|
|
@@ -2065,7 +2075,15 @@ var AccuracySchema = z2.object({
|
|
|
2065
2075
|
*/
|
|
2066
2076
|
searchTerms: z2.array(z2.string()).min(1).max(6)
|
|
2067
2077
|
})
|
|
2068
|
-
).max(MAX_ASSERTIONS)
|
|
2078
|
+
).max(MAX_ASSERTIONS),
|
|
2079
|
+
/** Does the answer describe MORE THAN ONE business under this name, or a
|
|
2080
|
+
* different business first? Defaulted, so a model that omits it reads as
|
|
2081
|
+
* "not detected" rather than failing the whole stage. */
|
|
2082
|
+
conflation: z2.object({
|
|
2083
|
+
detected: z2.boolean(),
|
|
2084
|
+
otherNames: z2.array(z2.string()).max(6),
|
|
2085
|
+
engineQuote: z2.string().nullable()
|
|
2086
|
+
}).default({ detected: false, otherNames: [], engineQuote: null })
|
|
2069
2087
|
});
|
|
2070
2088
|
function makeFenceTag2() {
|
|
2071
2089
|
return `data_${randomBytes2(6).toString("hex")}`;
|
|
@@ -2103,6 +2121,12 @@ For each assertion return:
|
|
|
2103
2121
|
- searchTerms: 1-6 distinctive words or short phrases you would search the site for to find this fact \u2014
|
|
2104
2122
|
proper nouns, numbers, street names. These are used to re-check "absent" verdicts across pages you were
|
|
2105
2123
|
not shown, so choose terms that would actually appear if the fact were stated somewhere else on the site.
|
|
2124
|
+
|
|
2125
|
+
Separately, return "conflation": does the answer describe MORE THAN ONE business under this name, or
|
|
2126
|
+
describe a different business first? If so, detected is true, otherNames lists the OTHER businesses
|
|
2127
|
+
named (never this one), and engineQuote is the exact sentence that says the name belongs to several
|
|
2128
|
+
businesses \u2014 copied verbatim, it is checked against the answer character by character. Otherwise
|
|
2129
|
+
detected is false with an empty otherNames and a null engineQuote.
|
|
2106
2130
|
Avoid generic words ("dental", "services") that appear on every page.
|
|
2107
2131
|
|
|
2108
2132
|
Be conservative about "confirmed": if the site only gestures at the fact, that is "absent". Being wrong in
|
|
@@ -2268,28 +2292,40 @@ function quoteSupportsClaim(claim, quote) {
|
|
|
2268
2292
|
for (const w of words(claim)) if (inQuote.has(w)) shared += 1;
|
|
2269
2293
|
return shared >= 2;
|
|
2270
2294
|
}
|
|
2271
|
-
function verifyQuotes(raw, answerText, siteText, answerOf, prospectDomain) {
|
|
2295
|
+
function verifyQuotes(raw, answerText, siteText, answerOf, prospectDomain, searchTermsByClaim = /* @__PURE__ */ new Map()) {
|
|
2272
2296
|
const answers = normalize(answerText);
|
|
2273
2297
|
const site = normalize(siteText);
|
|
2298
|
+
const flat = siteText.replace(/\s+/g, " ");
|
|
2274
2299
|
const out = [];
|
|
2275
2300
|
for (const a of raw) {
|
|
2276
2301
|
if (!answers.includes(normalize(a.engineQuote))) continue;
|
|
2302
|
+
let verdict = a.verdict;
|
|
2303
|
+
let siteQuote = a.siteQuote;
|
|
2304
|
+
if (a.verdict === "contradicted") {
|
|
2305
|
+
for (const term of distinctive(searchTermsByClaim.get(a.claim) ?? [])) {
|
|
2306
|
+
if (findTerm(site, term) !== "exact") continue;
|
|
2307
|
+
const at = site.indexOf(normalize(term));
|
|
2308
|
+
verdict = "confirmed";
|
|
2309
|
+
siteQuote = flat.slice(Math.max(0, at - 80), at + term.length + 80).trim();
|
|
2310
|
+
break;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2277
2313
|
const from = answerOf.get(normalize(a.engineQuote));
|
|
2278
2314
|
const sourceDomains = [
|
|
2279
2315
|
...new Set((from?.citedDomains ?? []).map(domainOf).filter((d) => d !== prospectDomain))
|
|
2280
2316
|
];
|
|
2281
|
-
const quoteReal =
|
|
2282
|
-
const needsQuote =
|
|
2283
|
-
const quoteSupports = quoteReal &&
|
|
2317
|
+
const quoteReal = siteQuote !== null && site.includes(normalize(siteQuote));
|
|
2318
|
+
const needsQuote = verdict === "confirmed" || verdict === "contradicted";
|
|
2319
|
+
const quoteSupports = quoteReal && siteQuote !== null && quoteSupportsClaim(a.claim, siteQuote);
|
|
2284
2320
|
const unsupported = needsQuote && (!quoteReal || !quoteSupports);
|
|
2285
2321
|
out.push({
|
|
2286
2322
|
claim: a.claim,
|
|
2287
|
-
verdict: unsupported ? "unverified" :
|
|
2323
|
+
verdict: unsupported ? "unverified" : verdict,
|
|
2288
2324
|
engineQuote: a.engineQuote,
|
|
2289
2325
|
// Kept when it is real, even where it does not carry the claim: the
|
|
2290
2326
|
// reader can see what we looked at and judge it themselves, which is a
|
|
2291
2327
|
// better position than being told we found nothing.
|
|
2292
|
-
siteQuote: quoteReal ?
|
|
2328
|
+
siteQuote: quoteReal ? siteQuote : null,
|
|
2293
2329
|
nearbyMention: null,
|
|
2294
2330
|
unverifiedReason: !unsupported ? null : !quoteReal ? "We could not find the passage this was based on, so we have not stated it either way." : "The passage we found is about the same subject but does not actually say this, so we have not stated it either way.",
|
|
2295
2331
|
sourceDomains,
|
|
@@ -2332,11 +2368,13 @@ async function checkAccuracy(url, crawl, answers, prospectPhones, deps) {
|
|
|
2332
2368
|
siteFullyRead: false,
|
|
2333
2369
|
pagesRead: 0,
|
|
2334
2370
|
pagesTotal: crawl.pages.length,
|
|
2335
|
-
answersRead: 0
|
|
2371
|
+
answersRead: 0,
|
|
2372
|
+
conflation: NO_CONFLATION
|
|
2336
2373
|
};
|
|
2337
2374
|
}
|
|
2338
2375
|
const input = buildAccuracyInput(crawl, branded);
|
|
2339
2376
|
const parsed = AccuracySchema.parse(await deps.run(input));
|
|
2377
|
+
const termsByClaim = new Map(parsed.assertions.map((a) => [a.claim, a.searchTerms]));
|
|
2340
2378
|
const answerOf = /* @__PURE__ */ new Map();
|
|
2341
2379
|
for (const a of branded) {
|
|
2342
2380
|
const text = normalize(a.fullAnswer ?? "");
|
|
@@ -2346,14 +2384,17 @@ async function checkAccuracy(url, crawl, answers, prospectPhones, deps) {
|
|
|
2346
2384
|
}
|
|
2347
2385
|
}
|
|
2348
2386
|
const siteText = fullSiteText(crawl);
|
|
2387
|
+
const answerText = branded.map((a) => a.fullAnswer ?? "").join("\n");
|
|
2349
2388
|
const verified = verifyQuotes(
|
|
2350
2389
|
parsed.assertions,
|
|
2351
|
-
|
|
2390
|
+
answerText,
|
|
2352
2391
|
siteText,
|
|
2353
2392
|
answerOf,
|
|
2354
|
-
domainOf(url)
|
|
2393
|
+
domainOf(url),
|
|
2394
|
+
termsByClaim
|
|
2355
2395
|
);
|
|
2356
|
-
const
|
|
2396
|
+
const c = parsed.conflation;
|
|
2397
|
+
const conflation = c.detected && c.engineQuote !== null && normalize(answerText).includes(normalize(c.engineQuote)) ? { detected: true, otherNames: c.otherNames, engineQuote: c.engineQuote } : NO_CONFLATION;
|
|
2357
2398
|
const sources = await classifyDomains(
|
|
2358
2399
|
url,
|
|
2359
2400
|
prospectPhones,
|
|
@@ -2366,7 +2407,8 @@ async function checkAccuracy(url, crawl, answers, prospectPhones, deps) {
|
|
|
2366
2407
|
siteFullyRead: input.fullyRead,
|
|
2367
2408
|
pagesRead: input.pagesRead,
|
|
2368
2409
|
pagesTotal: crawl.pages.length,
|
|
2369
|
-
answersRead: branded.length
|
|
2410
|
+
answersRead: branded.length,
|
|
2411
|
+
conflation
|
|
2370
2412
|
};
|
|
2371
2413
|
}
|
|
2372
2414
|
|
|
@@ -3155,6 +3197,99 @@ async function checkBasics(crawl, deps) {
|
|
|
3155
3197
|
};
|
|
3156
3198
|
}
|
|
3157
3199
|
|
|
3200
|
+
// src/prospect/measured-fixes.ts
|
|
3201
|
+
var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
3202
|
+
function measuredFixes(input) {
|
|
3203
|
+
const out = [];
|
|
3204
|
+
const c = input.checks;
|
|
3205
|
+
if (c && c.crawlerAccessMeasured && c.crawlerAccess.blockedAi.length > 0) {
|
|
3206
|
+
const names = c.crawlerAccess.blockedAi.join(", ");
|
|
3207
|
+
out.push({
|
|
3208
|
+
title: `Let ${names} read your site`,
|
|
3209
|
+
why: `Your robots.txt turns away ${names}. A crawler that is refused cannot read a page, so nothing else in this report can help for it until this changes.`,
|
|
3210
|
+
impact: "high",
|
|
3211
|
+
effort: "low",
|
|
3212
|
+
tier: "crawl",
|
|
3213
|
+
addresses: null,
|
|
3214
|
+
origin: "measured"
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
for (const r of input.goalFit?.requirements ?? []) {
|
|
3218
|
+
if (r.status !== "missing") continue;
|
|
3219
|
+
out.push({
|
|
3220
|
+
title: r.label.charAt(0).toUpperCase() + r.label.slice(1),
|
|
3221
|
+
why: r.why,
|
|
3222
|
+
impact: "high",
|
|
3223
|
+
effort: r.scope === "quick" ? "low" : r.scope === "content" ? "medium" : "high",
|
|
3224
|
+
tier: r.scope === "quick" ? "technical" : "content",
|
|
3225
|
+
addresses: r.key,
|
|
3226
|
+
origin: "measured"
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
const phoneJudgedByGoal = (input.goalFit?.requirements ?? []).some(
|
|
3230
|
+
(r) => r.key === "tappable-phone"
|
|
3231
|
+
);
|
|
3232
|
+
const plain = phoneJudgedByGoal ? 0 : (input.phones ?? []).filter((p) => p.linked === false).length;
|
|
3233
|
+
if (plain > 0) {
|
|
3234
|
+
out.push({
|
|
3235
|
+
title: "Make your phone number tappable",
|
|
3236
|
+
why: `${plural(plain, "number is", "numbers are")} written as plain text. On a phone that is something a visitor has to memorise and retype; as a link it is one tap, and it is the moment they were most likely to call.`,
|
|
3237
|
+
impact: "medium",
|
|
3238
|
+
effort: "low",
|
|
3239
|
+
tier: "technical",
|
|
3240
|
+
addresses: null,
|
|
3241
|
+
origin: "measured"
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
if (input.brokenLinks !== null && input.brokenLinks > 0) {
|
|
3245
|
+
out.push({
|
|
3246
|
+
title: `Repair ${plural(input.brokenLinks, "broken link", "broken links")}`,
|
|
3247
|
+
why: "A visitor who follows one lands on an error page, and a crawler that follows one stops there.",
|
|
3248
|
+
impact: "medium",
|
|
3249
|
+
effort: "low",
|
|
3250
|
+
tier: "technical",
|
|
3251
|
+
addresses: null,
|
|
3252
|
+
origin: "measured"
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
if (input.brokenImages !== null && input.brokenImages > 0) {
|
|
3256
|
+
out.push({
|
|
3257
|
+
title: `Replace ${plural(input.brokenImages, "broken image", "broken images")}`,
|
|
3258
|
+
why: "It shows as an empty box or a missing-image icon on the page a visitor is reading.",
|
|
3259
|
+
impact: "low",
|
|
3260
|
+
effort: "low",
|
|
3261
|
+
tier: "technical",
|
|
3262
|
+
addresses: null,
|
|
3263
|
+
origin: "measured"
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
if (c) {
|
|
3267
|
+
if (c.headings.pagesWithoutH1 > 0) {
|
|
3268
|
+
out.push({
|
|
3269
|
+
title: `Give ${plural(c.headings.pagesWithoutH1, "page", "pages")} a top heading`,
|
|
3270
|
+
why: `${c.headings.pagesWithoutH1} of ${c.meta.pageCount} pages have no top-level heading, so a reader or a crawler arriving on them is not told what the page is about.`,
|
|
3271
|
+
impact: "medium",
|
|
3272
|
+
effort: "low",
|
|
3273
|
+
tier: "technical",
|
|
3274
|
+
addresses: null,
|
|
3275
|
+
origin: "measured"
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
if (c.meta.missingCanonical > 0) {
|
|
3279
|
+
out.push({
|
|
3280
|
+
title: "Tell search engines which address is the real one for each page",
|
|
3281
|
+
why: `${c.meta.missingCanonical} of ${c.meta.pageCount} pages do not declare a canonical address. When the same page is reachable at more than one address, a search engine has to guess which one to keep.`,
|
|
3282
|
+
impact: "medium",
|
|
3283
|
+
effort: "low",
|
|
3284
|
+
tier: "technical",
|
|
3285
|
+
addresses: null,
|
|
3286
|
+
origin: "measured"
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
return out;
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3158
3293
|
// src/prospect/pipeline.ts
|
|
3159
3294
|
function envAnalyzeDeps(factories = {
|
|
3160
3295
|
api: defaultAnalyzeDeps,
|
|
@@ -3215,6 +3350,10 @@ async function stage(name, deps, fn) {
|
|
|
3215
3350
|
return { ok: false, error };
|
|
3216
3351
|
}
|
|
3217
3352
|
}
|
|
3353
|
+
function mergeFixes(measured, model) {
|
|
3354
|
+
const covered = new Set(measured.map((f) => f.addresses).filter((k) => !!k));
|
|
3355
|
+
return [...measured, ...model.filter((f) => !(f.addresses && covered.has(f.addresses)))];
|
|
3356
|
+
}
|
|
3218
3357
|
async function runProspectAudit(url, opts, deps = {}) {
|
|
3219
3358
|
const llmAuth = llmAuthMode();
|
|
3220
3359
|
const crawlDeps = deps.crawl ?? defaultCrawlDeps();
|
|
@@ -3320,9 +3459,21 @@ async function runProspectAudit(url, opts, deps = {}) {
|
|
|
3320
3459
|
checks.ok ? checks.data : null
|
|
3321
3460
|
)
|
|
3322
3461
|
};
|
|
3323
|
-
const reconciled = analyze.ok
|
|
3462
|
+
const reconciled = analyze.ok ? {
|
|
3324
3463
|
ok: true,
|
|
3325
|
-
data: {
|
|
3464
|
+
data: {
|
|
3465
|
+
...analyze.data,
|
|
3466
|
+
fixes: mergeFixes(
|
|
3467
|
+
measuredFixes({
|
|
3468
|
+
goalFit: goalFit.ok ? goalFit.data : null,
|
|
3469
|
+
checks: checks.ok ? checks.data : null,
|
|
3470
|
+
phones: checks.ok ? checks.data.consistency?.phones ?? null : null,
|
|
3471
|
+
brokenLinks: assets.ok ? assets.data.brokenLinks.length : null,
|
|
3472
|
+
brokenImages: assets.ok ? assets.data.brokenImages.length : null
|
|
3473
|
+
}),
|
|
3474
|
+
goalFit.ok ? reconcileFixes(analyze.data.fixes, goalFit.data) : analyze.data.fixes
|
|
3475
|
+
)
|
|
3476
|
+
}
|
|
3326
3477
|
} : analyze;
|
|
3327
3478
|
const brandedAnswers = probes.ok ? probes.data.answers.filter((a) => a.kind === "branded") : [];
|
|
3328
3479
|
const accuracy = brandedAnswers.length === 0 ? { ok: false, error: ACCURACY_SKIPPED } : await stage("accuracy", deps, async () => {
|
|
@@ -3377,6 +3528,7 @@ export {
|
|
|
3377
3528
|
GOAL_UNRESOLVED,
|
|
3378
3529
|
ASSET_ACCEPT,
|
|
3379
3530
|
defaultAssetProbe,
|
|
3531
|
+
mergeFixes,
|
|
3380
3532
|
runProspectAudit
|
|
3381
3533
|
};
|
|
3382
|
-
//# sourceMappingURL=chunk-
|
|
3534
|
+
//# sourceMappingURL=chunk-BJQCIJOY.js.map
|