pagesight 0.14.2 → 0.15.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/package.json +1 -1
- package/src/tools/ai.ts +33 -11
- package/src/tools/audit.ts +15 -0
- package/src/tools/page.ts +27 -4
- package/src/tools/search.ts +31 -7
- package/src/tools/speed.ts +53 -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,16 +924,39 @@ 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
|
|
930
|
-
? parsed.title.length >
|
|
931
|
-
? `${parsed.title.slice(0,
|
|
953
|
+
? parsed.title.length > 60
|
|
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,15 @@ 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 savingsStr = maxSavings ? `, up to ${maxSavings}` : "";
|
|
461
|
+
lines.push(` ${title} (${count}/${results.length} pages${savingsStr})`);
|
|
458
462
|
}
|
|
459
463
|
lines.push("");
|
|
460
464
|
}
|
|
461
465
|
|
|
462
466
|
// A11y failures deduplicated across pages
|
|
463
|
-
const a11yFailures = new Map<string, { title: string; count: number; pages: string[] }>();
|
|
467
|
+
const a11yFailures = new Map<string, { title: string; count: number; pages: string[]; selectors: string[] }>();
|
|
464
468
|
for (const { url, result } of results) {
|
|
465
469
|
const lhr = result.lighthouseResult;
|
|
466
470
|
const a11yCat = lhr.categories.accessibility;
|
|
@@ -469,13 +473,20 @@ function formatBatchTable(results: Array<{ url: string; result: PsiResult }>, st
|
|
|
469
473
|
for (const ref of a11yCat.auditRefs) {
|
|
470
474
|
const audit = lhr.audits[ref.id];
|
|
471
475
|
if (audit && audit.score !== null && audit.score < 1) {
|
|
472
|
-
const existing = a11yFailures.get(ref.id);
|
|
473
476
|
const page = shortUrl(url, allUrls);
|
|
477
|
+
const selector = audit.details?.items?.[0]?.node?.selector ?? null;
|
|
478
|
+
const existing = a11yFailures.get(ref.id);
|
|
474
479
|
if (existing) {
|
|
475
480
|
existing.count++;
|
|
476
481
|
existing.pages.push(page);
|
|
482
|
+
if (selector && !existing.selectors.includes(selector)) existing.selectors.push(selector);
|
|
477
483
|
} else {
|
|
478
|
-
a11yFailures.set(ref.id, {
|
|
484
|
+
a11yFailures.set(ref.id, {
|
|
485
|
+
title: audit.title,
|
|
486
|
+
count: 1,
|
|
487
|
+
pages: [page],
|
|
488
|
+
selectors: selector ? [selector] : [],
|
|
489
|
+
});
|
|
479
490
|
}
|
|
480
491
|
}
|
|
481
492
|
}
|
|
@@ -486,8 +497,11 @@ function formatBatchTable(results: Array<{ url: string; result: PsiResult }>, st
|
|
|
486
497
|
.sort((a, b) => b[1].count - a[1].count);
|
|
487
498
|
if (sharedA11y.length > 0) {
|
|
488
499
|
lines.push("--- Accessibility Issues (shared) ---", "");
|
|
489
|
-
for (const [, { title, count, pages }] of sharedA11y.slice(0, 10)) {
|
|
500
|
+
for (const [, { title, count, pages, selectors }] of sharedA11y.slice(0, 10)) {
|
|
490
501
|
lines.push(` ${title} (${count}/${results.length} pages: ${pages.join(", ")})`);
|
|
502
|
+
if (selectors.length > 0) {
|
|
503
|
+
lines.push(` Elements: ${selectors.slice(0, 3).join(", ")}`);
|
|
504
|
+
}
|
|
491
505
|
}
|
|
492
506
|
lines.push("");
|
|
493
507
|
}
|
|
@@ -601,6 +615,37 @@ function formatCrux(target: string, result: CruxResponse): string {
|
|
|
601
615
|
}
|
|
602
616
|
}
|
|
603
617
|
|
|
618
|
+
// Core Web Vitals assessment (Google ranking signal)
|
|
619
|
+
const cwvMetrics = r.metrics;
|
|
620
|
+
const lcp = cwvMetrics.largest_contentful_paint?.percentiles?.p75 as number | undefined;
|
|
621
|
+
const inp = cwvMetrics.interaction_to_next_paint?.percentiles?.p75 as number | undefined;
|
|
622
|
+
const cls = cwvMetrics.cumulative_layout_shift?.percentiles?.p75 as number | undefined;
|
|
623
|
+
|
|
624
|
+
if (lcp !== undefined || inp !== undefined || cls !== undefined) {
|
|
625
|
+
lines.push("", "--- Core Web Vitals Assessment ---", "");
|
|
626
|
+
const lcpPass = lcp !== undefined && lcp <= 2500;
|
|
627
|
+
const inpPass = inp !== undefined && inp <= 200;
|
|
628
|
+
const clsPass = cls !== undefined && cls <= 0.1;
|
|
629
|
+
|
|
630
|
+
if (lcp !== undefined)
|
|
631
|
+
lines.push(`LCP: ${lcp}ms ${lcpPass ? "GOOD" : lcp <= 4000 ? "NEEDS IMPROVEMENT" : "POOR"} (threshold: 2500ms)`);
|
|
632
|
+
if (inp !== undefined)
|
|
633
|
+
lines.push(`INP: ${inp}ms ${inpPass ? "GOOD" : inp <= 500 ? "NEEDS IMPROVEMENT" : "POOR"} (threshold: 200ms)`);
|
|
634
|
+
if (cls !== undefined)
|
|
635
|
+
lines.push(`CLS: ${cls} ${clsPass ? "GOOD" : cls <= 0.25 ? "NEEDS IMPROVEMENT" : "POOR"} (threshold: 0.1)`);
|
|
636
|
+
|
|
637
|
+
const allPresent = lcp !== undefined && inp !== undefined && cls !== undefined;
|
|
638
|
+
if (allPresent) {
|
|
639
|
+
const allPass = lcpPass && inpPass && clsPass;
|
|
640
|
+
lines.push(
|
|
641
|
+
"",
|
|
642
|
+
allPass
|
|
643
|
+
? "Overall: PASS — all Core Web Vitals are good (positive ranking signal)"
|
|
644
|
+
: "Overall: FAIL — not all Core Web Vitals pass (may affect rankings)",
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
604
649
|
return lines.join("\n");
|
|
605
650
|
}
|
|
606
651
|
|