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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagesight",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
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,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 > 40
931
- ? `${parsed.title.slice(0, 40)}...`
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: 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,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
- lines.push(` ${title} (${count}/${results.length} pages)`);
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, { title: audit.title, count: 1, pages: [page] });
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