pagesight 0.14.3 → 0.15.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/package.json +1 -1
- package/src/tools/ai.ts +33 -11
- package/src/tools/audit.ts +15 -0
- package/src/tools/page.ts +25 -2
- package/src/tools/search.ts +31 -7
- package/src/tools/speed.ts +54 -8
package/package.json
CHANGED
package/src/tools/ai.ts
CHANGED
|
@@ -8,6 +8,9 @@ interface LlmsTxtResult {
|
|
|
8
8
|
exists: boolean;
|
|
9
9
|
size: number | null;
|
|
10
10
|
firstLine: string | null;
|
|
11
|
+
lineCount: number | null;
|
|
12
|
+
sections: string[];
|
|
13
|
+
linkCount: number | null;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
async function checkLlmsTxt(origin: string, path: string): Promise<LlmsTxtResult> {
|
|
@@ -20,24 +23,37 @@ async function checkLlmsTxt(origin: string, path: string): Promise<LlmsTxtResult
|
|
|
20
23
|
signal: controller.signal,
|
|
21
24
|
});
|
|
22
25
|
clearTimeout(timeout);
|
|
23
|
-
if (!res.ok) return { exists: false, size: null, firstLine: null };
|
|
26
|
+
if (!res.ok) return { exists: false, size: null, firstLine: null, lineCount: null, sections: [], linkCount: null };
|
|
24
27
|
// Cap at 1MB to avoid OOM on large responses
|
|
25
28
|
const contentLength = Number(res.headers.get("content-length") ?? 0);
|
|
26
29
|
if (contentLength > 1_048_576) {
|
|
27
|
-
return {
|
|
30
|
+
return {
|
|
31
|
+
exists: true,
|
|
32
|
+
size: contentLength,
|
|
33
|
+
firstLine: "(file too large to preview)",
|
|
34
|
+
lineCount: null,
|
|
35
|
+
sections: [],
|
|
36
|
+
linkCount: null,
|
|
37
|
+
};
|
|
28
38
|
}
|
|
29
39
|
const text = await res.text();
|
|
30
40
|
if (text.length > 1_048_576) {
|
|
31
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
exists: true,
|
|
43
|
+
size: text.length,
|
|
44
|
+
firstLine: "(file too large to preview)",
|
|
45
|
+
lineCount: null,
|
|
46
|
+
sections: [],
|
|
47
|
+
linkCount: null,
|
|
48
|
+
};
|
|
32
49
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return { exists: true, size: text.length, firstLine };
|
|
50
|
+
const textLines = text.split("\n");
|
|
51
|
+
const firstLine = textLines.find((l) => l.trim().length > 0)?.trim() ?? null;
|
|
52
|
+
const sections = textLines.filter((l) => /^##\s/.test(l)).map((l) => l.replace(/^##\s+/, "").trim());
|
|
53
|
+
const linkCount = (text.match(/\[.*?\]\(https?:\/\/[^)]+\)/g) ?? []).length;
|
|
54
|
+
return { exists: true, size: text.length, firstLine, lineCount: textLines.length, sections, linkCount };
|
|
39
55
|
} catch {
|
|
40
|
-
return { exists: false, size: null, firstLine: null };
|
|
56
|
+
return { exists: false, size: null, firstLine: null, lineCount: null, sections: [], linkCount: null };
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
59
|
|
|
@@ -46,8 +62,14 @@ function formatLlmsTxt(llmsTxt: LlmsTxtResult, llmsFullTxt: LlmsTxtResult): stri
|
|
|
46
62
|
|
|
47
63
|
if (llmsTxt.exists) {
|
|
48
64
|
const sizeKB = llmsTxt.size ? `${(llmsTxt.size / 1024).toFixed(1)} KB` : "unknown size";
|
|
49
|
-
|
|
65
|
+
const details = [sizeKB];
|
|
66
|
+
if (llmsTxt.lineCount) details.push(`${llmsTxt.lineCount} lines`);
|
|
67
|
+
if (llmsTxt.linkCount) details.push(`${llmsTxt.linkCount} links`);
|
|
68
|
+
lines.push(`llms.txt: FOUND (${details.join(", ")})`);
|
|
50
69
|
if (llmsTxt.firstLine) lines.push(` ${llmsTxt.firstLine}`);
|
|
70
|
+
if (llmsTxt.sections.length > 0) {
|
|
71
|
+
lines.push(` Sections: ${llmsTxt.sections.join(", ")}`);
|
|
72
|
+
}
|
|
51
73
|
} else {
|
|
52
74
|
lines.push("llms.txt: NOT FOUND — consider adding one for AI-friendly documentation");
|
|
53
75
|
}
|
package/src/tools/audit.ts
CHANGED
|
@@ -382,6 +382,21 @@ export function registerAuditTool(server: McpServer): void {
|
|
|
382
382
|
if (inspectResult.status === "fulfilled" && inspectResult.value) {
|
|
383
383
|
const idx = inspectResult.value.indexStatusResult;
|
|
384
384
|
addInspectFindings(idx.verdict, idx.coverageState, findings);
|
|
385
|
+
|
|
386
|
+
// Rich Results check
|
|
387
|
+
const richResults = inspectResult.value.richResultsResult;
|
|
388
|
+
if (richResults && richResults.verdict === "FAIL") {
|
|
389
|
+
const failingTypes = (richResults.detectedItems ?? [])
|
|
390
|
+
.filter((item) => item.items?.some((i) => (i.issues?.length ?? 0) > 0))
|
|
391
|
+
.map((item) => item.richResultType);
|
|
392
|
+
if (failingTypes.length > 0) {
|
|
393
|
+
findings.push({
|
|
394
|
+
severity: "MEDIUM",
|
|
395
|
+
message: `Rich Results failing for: ${failingTypes.join(", ")}`,
|
|
396
|
+
source: "inspect",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
385
400
|
} else if (inspectResult.status === "rejected") {
|
|
386
401
|
errors.push(`Inspect: ${inspectResult.reason}`);
|
|
387
402
|
}
|
package/src/tools/page.ts
CHANGED
|
@@ -924,6 +924,29 @@ export function registerPageTool(server: McpServer): void {
|
|
|
924
924
|
})
|
|
925
925
|
.filter(Boolean);
|
|
926
926
|
|
|
927
|
+
// Run structured data validation for batch summary
|
|
928
|
+
let jsonLdSummary = "none";
|
|
929
|
+
if (parsed.jsonLd.length > 0) {
|
|
930
|
+
const { issues: valIssues } = validateJsonLd(parsed.jsonLd);
|
|
931
|
+
const requiredMissing = valIssues.filter((i) => i.level === "required").length;
|
|
932
|
+
if (jsonLdTypes.length > 0) {
|
|
933
|
+
jsonLdSummary = jsonLdTypes.join(", ");
|
|
934
|
+
if (requiredMissing > 0) {
|
|
935
|
+
jsonLdSummary += ` (${requiredMissing} required missing)`;
|
|
936
|
+
issues.push(`${requiredMissing} missing required fields in JSON-LD`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Description with length
|
|
942
|
+
const descVal = getMeta(parsed.meta, "description");
|
|
943
|
+
let descSummary: string;
|
|
944
|
+
if (descVal) {
|
|
945
|
+
descSummary = descVal.length > 155 ? `yes (${descVal.length} chars ⚠)` : `yes (${descVal.length})`;
|
|
946
|
+
} else {
|
|
947
|
+
descSummary = "no";
|
|
948
|
+
}
|
|
949
|
+
|
|
927
950
|
results.push({
|
|
928
951
|
url: finalUrl,
|
|
929
952
|
title: parsed.title
|
|
@@ -931,9 +954,9 @@ export function registerPageTool(server: McpServer): void {
|
|
|
931
954
|
? `${parsed.title.slice(0, parsed.title.lastIndexOf(" ", 60) > 20 ? parsed.title.lastIndexOf(" ", 60) : 60)}...`
|
|
932
955
|
: parsed.title
|
|
933
956
|
: "(missing)",
|
|
934
|
-
description:
|
|
957
|
+
description: descSummary,
|
|
935
958
|
canonical: parsed.canonical ? "yes" : "no",
|
|
936
|
-
jsonLd:
|
|
959
|
+
jsonLd: jsonLdSummary,
|
|
937
960
|
links: linkSummary,
|
|
938
961
|
issues,
|
|
939
962
|
});
|
package/src/tools/search.ts
CHANGED
|
@@ -302,6 +302,10 @@ function formatPerformance(
|
|
|
302
302
|
|
|
303
303
|
const totals = computeTotals(rows);
|
|
304
304
|
|
|
305
|
+
if (totals.impressions < 10) {
|
|
306
|
+
lines.push("⚠ Low data volume — trends may not be meaningful.", "");
|
|
307
|
+
}
|
|
308
|
+
|
|
305
309
|
lines.push(
|
|
306
310
|
`--- Summary (${rows.length} rows returned) ---`,
|
|
307
311
|
"",
|
|
@@ -351,15 +355,35 @@ function formatComparison(
|
|
|
351
355
|
`Previous: ${previousStart} to ${previousEnd}`,
|
|
352
356
|
`Dimensions: ${dimensions.join(", ")}`,
|
|
353
357
|
"",
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
if (cur.impressions < 10 && prev.impressions < 10) {
|
|
361
|
+
lines.push("⚠ Low data volume — trends may not be meaningful.", "");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
lines.push("--- Summary ---", "");
|
|
365
|
+
lines.push(" Current Previous Change");
|
|
366
|
+
lines.push(
|
|
357
367
|
`Clicks: ${String(cur.clicks.toLocaleString()).padEnd(12)} ${String(prev.clicks.toLocaleString()).padEnd(12)} ${pctChange(cur.clicks, prev.clicks)}`,
|
|
368
|
+
);
|
|
369
|
+
lines.push(
|
|
358
370
|
`Impressions: ${String(cur.impressions.toLocaleString()).padEnd(12)} ${String(prev.impressions.toLocaleString()).padEnd(12)} ${pctChange(cur.impressions, prev.impressions)}`,
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
371
|
+
);
|
|
372
|
+
const curCtr = `${(cur.ctr * 100).toFixed(1)}%`;
|
|
373
|
+
const prevCtr = `${(prev.ctr * 100).toFixed(1)}%`;
|
|
374
|
+
lines.push(`Avg CTR: ${curCtr.padEnd(12)} ${prevCtr.padEnd(12)} ${((cur.ctr - prev.ctr) * 100).toFixed(1)}pp`);
|
|
375
|
+
const posStatus =
|
|
376
|
+
prev.position === 0 && cur.position > 0
|
|
377
|
+
? "new"
|
|
378
|
+
: cur.position < prev.position
|
|
379
|
+
? "improved"
|
|
380
|
+
: cur.position > prev.position
|
|
381
|
+
? "regressed"
|
|
382
|
+
: "stable";
|
|
383
|
+
lines.push(
|
|
384
|
+
`Avg Position:${String(cur.position.toFixed(1)).padEnd(13)} ${String(prev.position.toFixed(1)).padEnd(12)} ${posStatus} (${(cur.position - prev.position).toFixed(1)})`,
|
|
385
|
+
);
|
|
386
|
+
lines.push("");
|
|
363
387
|
|
|
364
388
|
// Build lookup for previous period rows
|
|
365
389
|
const prevMap = new Map<string, (typeof prevRows)[0]>();
|
package/src/tools/speed.ts
CHANGED
|
@@ -438,14 +438,17 @@ function formatBatchTable(results: Array<{ url: string; result: PsiResult }>, st
|
|
|
438
438
|
lines.push("");
|
|
439
439
|
|
|
440
440
|
// Shared opportunities across pages
|
|
441
|
-
const oppCounts = new Map<string, { title: string; count: number }>();
|
|
441
|
+
const oppCounts = new Map<string, { title: string; count: number; maxSavings: string }>();
|
|
442
442
|
for (const { result } of results) {
|
|
443
443
|
for (const id of collectOpportunityIds(result.lighthouseResult.audits)) {
|
|
444
|
+
const audit = result.lighthouseResult.audits[id];
|
|
445
|
+
const savings = audit.displayValue ?? "";
|
|
444
446
|
const existing = oppCounts.get(id);
|
|
445
447
|
if (existing) {
|
|
446
448
|
existing.count++;
|
|
449
|
+
if (savings && (!existing.maxSavings || savings > existing.maxSavings)) existing.maxSavings = savings;
|
|
447
450
|
} else {
|
|
448
|
-
oppCounts.set(id, { title:
|
|
451
|
+
oppCounts.set(id, { title: audit.title, count: 1, maxSavings: savings });
|
|
449
452
|
}
|
|
450
453
|
}
|
|
451
454
|
}
|
|
@@ -453,14 +456,16 @@ function formatBatchTable(results: Array<{ url: string; result: PsiResult }>, st
|
|
|
453
456
|
const sharedOpps = [...oppCounts.entries()].filter(([, v]) => v.count >= 2).sort((a, b) => b[1].count - a[1].count);
|
|
454
457
|
if (sharedOpps.length > 0) {
|
|
455
458
|
lines.push("--- Shared Opportunities ---", "");
|
|
456
|
-
for (const [, { title, count }] of sharedOpps.slice(0, 10)) {
|
|
457
|
-
|
|
459
|
+
for (const [, { title, count, maxSavings }] of sharedOpps.slice(0, 10)) {
|
|
460
|
+
const cleaned = maxSavings.replace(/^Est savings of /i, "");
|
|
461
|
+
const savingsStr = cleaned ? `, up to ${cleaned}` : "";
|
|
462
|
+
lines.push(` ${title} (${count}/${results.length} pages${savingsStr})`);
|
|
458
463
|
}
|
|
459
464
|
lines.push("");
|
|
460
465
|
}
|
|
461
466
|
|
|
462
467
|
// A11y failures deduplicated across pages
|
|
463
|
-
const a11yFailures = new Map<string, { title: string; count: number; pages: string[] }>();
|
|
468
|
+
const a11yFailures = new Map<string, { title: string; count: number; pages: string[]; selectors: string[] }>();
|
|
464
469
|
for (const { url, result } of results) {
|
|
465
470
|
const lhr = result.lighthouseResult;
|
|
466
471
|
const a11yCat = lhr.categories.accessibility;
|
|
@@ -469,13 +474,20 @@ function formatBatchTable(results: Array<{ url: string; result: PsiResult }>, st
|
|
|
469
474
|
for (const ref of a11yCat.auditRefs) {
|
|
470
475
|
const audit = lhr.audits[ref.id];
|
|
471
476
|
if (audit && audit.score !== null && audit.score < 1) {
|
|
472
|
-
const existing = a11yFailures.get(ref.id);
|
|
473
477
|
const page = shortUrl(url, allUrls);
|
|
478
|
+
const selector = audit.details?.items?.[0]?.node?.selector ?? null;
|
|
479
|
+
const existing = a11yFailures.get(ref.id);
|
|
474
480
|
if (existing) {
|
|
475
481
|
existing.count++;
|
|
476
482
|
existing.pages.push(page);
|
|
483
|
+
if (selector && !existing.selectors.includes(selector)) existing.selectors.push(selector);
|
|
477
484
|
} else {
|
|
478
|
-
a11yFailures.set(ref.id, {
|
|
485
|
+
a11yFailures.set(ref.id, {
|
|
486
|
+
title: audit.title,
|
|
487
|
+
count: 1,
|
|
488
|
+
pages: [page],
|
|
489
|
+
selectors: selector ? [selector] : [],
|
|
490
|
+
});
|
|
479
491
|
}
|
|
480
492
|
}
|
|
481
493
|
}
|
|
@@ -486,8 +498,11 @@ function formatBatchTable(results: Array<{ url: string; result: PsiResult }>, st
|
|
|
486
498
|
.sort((a, b) => b[1].count - a[1].count);
|
|
487
499
|
if (sharedA11y.length > 0) {
|
|
488
500
|
lines.push("--- Accessibility Issues (shared) ---", "");
|
|
489
|
-
for (const [, { title, count, pages }] of sharedA11y.slice(0, 10)) {
|
|
501
|
+
for (const [, { title, count, pages, selectors }] of sharedA11y.slice(0, 10)) {
|
|
490
502
|
lines.push(` ${title} (${count}/${results.length} pages: ${pages.join(", ")})`);
|
|
503
|
+
if (selectors.length > 0) {
|
|
504
|
+
lines.push(` Elements: ${selectors.slice(0, 3).join(", ")}`);
|
|
505
|
+
}
|
|
491
506
|
}
|
|
492
507
|
lines.push("");
|
|
493
508
|
}
|
|
@@ -601,6 +616,37 @@ function formatCrux(target: string, result: CruxResponse): string {
|
|
|
601
616
|
}
|
|
602
617
|
}
|
|
603
618
|
|
|
619
|
+
// Core Web Vitals assessment (Google ranking signal)
|
|
620
|
+
const cwvMetrics = r.metrics;
|
|
621
|
+
const lcp = cwvMetrics.largest_contentful_paint?.percentiles?.p75 as number | undefined;
|
|
622
|
+
const inp = cwvMetrics.interaction_to_next_paint?.percentiles?.p75 as number | undefined;
|
|
623
|
+
const cls = cwvMetrics.cumulative_layout_shift?.percentiles?.p75 as number | undefined;
|
|
624
|
+
|
|
625
|
+
if (lcp !== undefined || inp !== undefined || cls !== undefined) {
|
|
626
|
+
lines.push("", "--- Core Web Vitals Assessment ---", "");
|
|
627
|
+
const lcpPass = lcp !== undefined && lcp <= 2500;
|
|
628
|
+
const inpPass = inp !== undefined && inp <= 200;
|
|
629
|
+
const clsPass = cls !== undefined && cls <= 0.1;
|
|
630
|
+
|
|
631
|
+
if (lcp !== undefined)
|
|
632
|
+
lines.push(`LCP: ${lcp}ms ${lcpPass ? "GOOD" : lcp <= 4000 ? "NEEDS IMPROVEMENT" : "POOR"} (threshold: 2500ms)`);
|
|
633
|
+
if (inp !== undefined)
|
|
634
|
+
lines.push(`INP: ${inp}ms ${inpPass ? "GOOD" : inp <= 500 ? "NEEDS IMPROVEMENT" : "POOR"} (threshold: 200ms)`);
|
|
635
|
+
if (cls !== undefined)
|
|
636
|
+
lines.push(`CLS: ${cls} ${clsPass ? "GOOD" : cls <= 0.25 ? "NEEDS IMPROVEMENT" : "POOR"} (threshold: 0.1)`);
|
|
637
|
+
|
|
638
|
+
const allPresent = lcp !== undefined && inp !== undefined && cls !== undefined;
|
|
639
|
+
if (allPresent) {
|
|
640
|
+
const allPass = lcpPass && inpPass && clsPass;
|
|
641
|
+
lines.push(
|
|
642
|
+
"",
|
|
643
|
+
allPass
|
|
644
|
+
? "Overall: PASS — all Core Web Vitals are good (positive ranking signal)"
|
|
645
|
+
: "Overall: FAIL — not all Core Web Vitals pass (may affect rankings)",
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
604
650
|
return lines.join("\n");
|
|
605
651
|
}
|
|
606
652
|
|