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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagesight",
3
- "version": "0.14.3",
3
+ "version": "0.15.1",
4
4
  "description": "See your site the way search engines and AI see it.",
5
5
  "keywords": [
6
6
  "seo",
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 { exists: true, size: contentLength, firstLine: "(file too large to preview)" };
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 { exists: true, size: text.length, firstLine: "(file too large to preview)" };
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 firstLine =
34
- text
35
- .split("\n")
36
- .find((l) => l.trim().length > 0)
37
- ?.trim() ?? null;
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
- lines.push(`llms.txt: FOUND (${sizeKB})`);
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
  }
@@ -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: getMeta(parsed.meta, "description") ? "yes" : "no",
957
+ description: descSummary,
935
958
  canonical: parsed.canonical ? "yes" : "no",
936
- jsonLd: jsonLdTypes.length > 0 ? jsonLdTypes.join(", ") : "none",
959
+ jsonLd: jsonLdSummary,
937
960
  links: linkSummary,
938
961
  issues,
939
962
  });
@@ -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
- "--- Summary ---",
355
- "",
356
- " Current Previous Change",
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
- `Avg CTR: ${`${(cur.ctr * 100).toFixed(1)}%`.padEnd(12)} ${`${(prev.ctr * 100).toFixed(1)}%`.padEnd(12)} ${((cur.ctr - prev.ctr) * 100).toFixed(1)}pp`,
360
- `Avg Position:${String(cur.position.toFixed(1)).padEnd(13)} ${String(prev.position.toFixed(1)).padEnd(12)} ${prev.position === 0 && cur.position > 0 ? "new" : cur.position < prev.position ? "improved" : cur.position > prev.position ? "regressed" : "stable"} (${(cur.position - prev.position).toFixed(1)})`,
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]>();
@@ -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: result.lighthouseResult.audits[id].title, count: 1 });
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
- lines.push(` ${title} (${count}/${results.length} pages)`);
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, { title: audit.title, count: 1, pages: [page] });
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