cbrowser 18.13.4 → 18.14.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/agent-personas.d.ts +55 -0
- package/dist/agent-personas.d.ts.map +1 -0
- package/dist/agent-personas.js +252 -0
- package/dist/agent-personas.js.map +1 -0
- package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
- package/dist/analysis/agent-ready-audit.js +584 -0
- package/dist/analysis/agent-ready-audit.js.map +1 -1
- package/dist/analysis/competitive-benchmark.d.ts +14 -0
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
- package/dist/analysis/competitive-benchmark.js +245 -0
- package/dist/analysis/competitive-benchmark.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/llms-txt/diff.d.ts +85 -0
- package/dist/llms-txt/diff.d.ts.map +1 -0
- package/dist/llms-txt/diff.js +234 -0
- package/dist/llms-txt/diff.js.map +1 -0
- package/dist/llms-txt/index.d.ts +19 -0
- package/dist/llms-txt/index.d.ts.map +1 -0
- package/dist/llms-txt/index.js +19 -0
- package/dist/llms-txt/index.js.map +1 -0
- package/dist/llms-txt/validator.d.ts +80 -0
- package/dist/llms-txt/validator.d.ts.map +1 -0
- package/dist/llms-txt/validator.js +341 -0
- package/dist/llms-txt/validator.js.map +1 -0
- package/dist/mcp-server-remote.d.ts.map +1 -1
- package/dist/mcp-server-remote.js +36 -13
- package/dist/mcp-server-remote.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +26 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools/base/analysis-tools.d.ts +1 -1
- package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/analysis-tools.js +56 -2
- package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.js +14 -1
- package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
- package/dist/mcp-tools/base/index.d.ts +7 -3
- package/dist/mcp-tools/base/index.d.ts.map +1 -1
- package/dist/mcp-tools/base/index.js +14 -4
- package/dist/mcp-tools/base/index.js.map +1 -1
- package/dist/mcp-tools/base/llms-txt-tools.d.ts +12 -0
- package/dist/mcp-tools/base/llms-txt-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/llms-txt-tools.js +119 -0
- package/dist/mcp-tools/base/llms-txt-tools.js.map +1 -0
- package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/persona-comparison-tools.js +12 -2
- package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
- package/dist/mcp-tools/base/remediation-tools.d.ts +12 -0
- package/dist/mcp-tools/base/remediation-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/remediation-tools.js +106 -0
- package/dist/mcp-tools/base/remediation-tools.js.map +1 -0
- package/dist/mcp-tools/index.d.ts +10 -10
- package/dist/mcp-tools/index.d.ts.map +1 -1
- package/dist/mcp-tools/index.js +11 -11
- package/dist/mcp-tools/index.js.map +1 -1
- package/dist/persona-questionnaire.d.ts +1 -1
- package/dist/persona-questionnaire.d.ts.map +1 -1
- package/dist/persona-questionnaire.js +12 -2
- package/dist/persona-questionnaire.js.map +1 -1
- package/dist/personas.d.ts +4 -2
- package/dist/personas.d.ts.map +1 -1
- package/dist/personas.js +12 -1
- package/dist/personas.js.map +1 -1
- package/dist/remediation/index.d.ts +19 -0
- package/dist/remediation/index.d.ts.map +1 -0
- package/dist/remediation/index.js +19 -0
- package/dist/remediation/index.js.map +1 -0
- package/dist/remediation/llms-txt.d.ts +65 -0
- package/dist/remediation/llms-txt.d.ts.map +1 -0
- package/dist/remediation/llms-txt.js +219 -0
- package/dist/remediation/llms-txt.js.map +1 -0
- package/dist/remediation/patch-generator.d.ts +54 -0
- package/dist/remediation/patch-generator.d.ts.map +1 -0
- package/dist/remediation/patch-generator.js +274 -0
- package/dist/remediation/patch-generator.js.map +1 -0
- package/dist/remediation/structured-data.d.ts +64 -0
- package/dist/remediation/structured-data.d.ts.map +1 -0
- package/dist/remediation/structured-data.js +352 -0
- package/dist/remediation/structured-data.js.map +1 -0
- package/dist/types.d.ts +213 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -381,6 +381,575 @@ async function detectLowFindabilityElements(ctx) {
|
|
|
381
381
|
summary.totalElements += poorSelectors.length;
|
|
382
382
|
}
|
|
383
383
|
// ============================================================================
|
|
384
|
+
// AI-Specific Detection Functions (v17.0.0)
|
|
385
|
+
// ============================================================================
|
|
386
|
+
/**
|
|
387
|
+
* Detect machine-readable metadata (JSON-LD, OpenGraph, Twitter Cards, landmarks)
|
|
388
|
+
* @since 17.0.0
|
|
389
|
+
*/
|
|
390
|
+
async function detectMachineMetadata(ctx) {
|
|
391
|
+
const { page, issues, summary } = ctx;
|
|
392
|
+
const metadata = await page.evaluate(() => {
|
|
393
|
+
// Check for JSON-LD
|
|
394
|
+
const jsonLdScripts = document.querySelectorAll('script[type="application/ld+json"]');
|
|
395
|
+
const hasJsonLd = jsonLdScripts.length > 0;
|
|
396
|
+
let jsonLdValid = true;
|
|
397
|
+
jsonLdScripts.forEach(script => {
|
|
398
|
+
try {
|
|
399
|
+
JSON.parse(script.textContent || '');
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
jsonLdValid = false;
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
// Check for OpenGraph tags
|
|
406
|
+
const ogTags = document.querySelectorAll('meta[property^="og:"]');
|
|
407
|
+
const hasOg = ogTags.length > 0;
|
|
408
|
+
const ogTitle = document.querySelector('meta[property="og:title"]');
|
|
409
|
+
const ogDescription = document.querySelector('meta[property="og:description"]');
|
|
410
|
+
// Check for Twitter Cards
|
|
411
|
+
const twitterTags = document.querySelectorAll('meta[name^="twitter:"]');
|
|
412
|
+
const hasTwitter = twitterTags.length > 0;
|
|
413
|
+
// Check for semantic landmarks
|
|
414
|
+
const hasMain = !!document.querySelector('main, [role="main"]');
|
|
415
|
+
const hasNav = !!document.querySelector('nav, [role="navigation"]');
|
|
416
|
+
const hasHeader = !!document.querySelector('header, [role="banner"]');
|
|
417
|
+
const hasFooter = !!document.querySelector('footer, [role="contentinfo"]');
|
|
418
|
+
return {
|
|
419
|
+
jsonLd: { present: hasJsonLd, valid: jsonLdValid, count: jsonLdScripts.length },
|
|
420
|
+
og: { present: hasOg, hasTitle: !!ogTitle, hasDescription: !!ogDescription, count: ogTags.length },
|
|
421
|
+
twitter: { present: hasTwitter, count: twitterTags.length },
|
|
422
|
+
landmarks: { main: hasMain, nav: hasNav, header: hasHeader, footer: hasFooter },
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
// Track metadata count
|
|
426
|
+
let metadataCount = 0;
|
|
427
|
+
if (metadata.jsonLd.present)
|
|
428
|
+
metadataCount++;
|
|
429
|
+
if (metadata.og.present)
|
|
430
|
+
metadataCount++;
|
|
431
|
+
if (metadata.twitter.present)
|
|
432
|
+
metadataCount++;
|
|
433
|
+
summary.machineMetadataCount = metadataCount;
|
|
434
|
+
// Report missing JSON-LD
|
|
435
|
+
if (!metadata.jsonLd.present) {
|
|
436
|
+
issues.push({
|
|
437
|
+
category: "semantics",
|
|
438
|
+
severity: "medium",
|
|
439
|
+
subcategory: "machine-metadata",
|
|
440
|
+
element: "head",
|
|
441
|
+
description: "No JSON-LD structured data found",
|
|
442
|
+
detectionMethod: "json-ld-check",
|
|
443
|
+
recommendation: "Add JSON-LD schema markup for better AI agent understanding",
|
|
444
|
+
codeExample: `<script type="application/ld+json">
|
|
445
|
+
{
|
|
446
|
+
"@context": "https://schema.org",
|
|
447
|
+
"@type": "WebPage",
|
|
448
|
+
"name": "Page Title",
|
|
449
|
+
"description": "Page description"
|
|
450
|
+
}
|
|
451
|
+
</script>`,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
else if (!metadata.jsonLd.valid) {
|
|
455
|
+
issues.push({
|
|
456
|
+
category: "semantics",
|
|
457
|
+
severity: "high",
|
|
458
|
+
subcategory: "machine-metadata",
|
|
459
|
+
element: "script[type='application/ld+json']",
|
|
460
|
+
description: "Invalid JSON-LD (malformed JSON)",
|
|
461
|
+
detectionMethod: "json-ld-check",
|
|
462
|
+
recommendation: "Fix JSON syntax errors in structured data",
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
// Report missing OpenGraph
|
|
466
|
+
if (!metadata.og.present) {
|
|
467
|
+
issues.push({
|
|
468
|
+
category: "semantics",
|
|
469
|
+
severity: "low",
|
|
470
|
+
subcategory: "machine-metadata",
|
|
471
|
+
element: "head",
|
|
472
|
+
description: "No OpenGraph meta tags found",
|
|
473
|
+
detectionMethod: "og-check",
|
|
474
|
+
recommendation: "Add OpenGraph tags for better content previews",
|
|
475
|
+
codeExample: `<meta property="og:title" content="Page Title">
|
|
476
|
+
<meta property="og:description" content="Page description">
|
|
477
|
+
<meta property="og:image" content="https://example.com/image.jpg">`,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
// Report missing landmarks
|
|
481
|
+
const missingLandmarks = [];
|
|
482
|
+
if (!metadata.landmarks.main)
|
|
483
|
+
missingLandmarks.push("main");
|
|
484
|
+
if (!metadata.landmarks.nav)
|
|
485
|
+
missingLandmarks.push("nav");
|
|
486
|
+
if (missingLandmarks.length > 0) {
|
|
487
|
+
issues.push({
|
|
488
|
+
category: "accessibility",
|
|
489
|
+
severity: "medium",
|
|
490
|
+
subcategory: "machine-metadata",
|
|
491
|
+
element: "body",
|
|
492
|
+
description: `Missing semantic landmarks: ${missingLandmarks.join(", ")}`,
|
|
493
|
+
detectionMethod: "landmark-check",
|
|
494
|
+
recommendation: "Add semantic landmark elements for page structure",
|
|
495
|
+
codeExample: `<header role="banner">...</header>
|
|
496
|
+
<nav role="navigation">...</nav>
|
|
497
|
+
<main role="main">...</main>
|
|
498
|
+
<footer role="contentinfo">...</footer>`,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Detect navigation patterns (breadcrumbs, skip links, heading hierarchy)
|
|
504
|
+
* @since 17.0.0
|
|
505
|
+
*/
|
|
506
|
+
async function detectNavigationPatterns(ctx) {
|
|
507
|
+
const { page, issues, summary } = ctx;
|
|
508
|
+
const navPatterns = await page.evaluate(() => {
|
|
509
|
+
// Check for breadcrumbs
|
|
510
|
+
const breadcrumbNav = document.querySelector('nav[aria-label*="breadcrumb" i], nav[aria-label*="Breadcrumb" i], [role="navigation"][aria-label*="breadcrumb" i]');
|
|
511
|
+
const breadcrumbSchema = document.querySelector('[itemtype*="BreadcrumbList"]');
|
|
512
|
+
const hasBreadcrumbs = !!breadcrumbNav || !!breadcrumbSchema;
|
|
513
|
+
// Check for skip links
|
|
514
|
+
const skipLinks = document.querySelectorAll('a[href^="#"]:first-child, a[href^="#main"], a[href^="#content"], .skip-link, .skip-to-content');
|
|
515
|
+
const hasSkipLink = skipLinks.length > 0;
|
|
516
|
+
// Check heading hierarchy
|
|
517
|
+
const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
|
518
|
+
const headingLevels = headings.map(h => parseInt(h.tagName[1]));
|
|
519
|
+
let hierarchyValid = true;
|
|
520
|
+
let hierarchyIssue = '';
|
|
521
|
+
const h1Count = headingLevels.filter(l => l === 1).length;
|
|
522
|
+
if (h1Count === 0) {
|
|
523
|
+
hierarchyValid = false;
|
|
524
|
+
hierarchyIssue = 'No H1 found';
|
|
525
|
+
}
|
|
526
|
+
else if (h1Count > 1) {
|
|
527
|
+
hierarchyValid = false;
|
|
528
|
+
hierarchyIssue = `Multiple H1 elements (${h1Count})`;
|
|
529
|
+
}
|
|
530
|
+
// Check for skipped levels (e.g., h1 -> h3)
|
|
531
|
+
for (let i = 1; i < headingLevels.length; i++) {
|
|
532
|
+
if (headingLevels[i] > headingLevels[i - 1] + 1) {
|
|
533
|
+
hierarchyValid = false;
|
|
534
|
+
hierarchyIssue = `Skipped heading level (h${headingLevels[i - 1]} to h${headingLevels[i]})`;
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Check page title
|
|
539
|
+
const pageTitle = document.title?.trim();
|
|
540
|
+
return {
|
|
541
|
+
breadcrumbs: hasBreadcrumbs,
|
|
542
|
+
skipLink: hasSkipLink,
|
|
543
|
+
headings: { valid: hierarchyValid, issue: hierarchyIssue, count: headings.length },
|
|
544
|
+
pageTitle: { present: !!pageTitle, length: pageTitle?.length || 0 },
|
|
545
|
+
};
|
|
546
|
+
});
|
|
547
|
+
// Track navigation aids
|
|
548
|
+
let navAidsCount = 0;
|
|
549
|
+
if (navPatterns.breadcrumbs)
|
|
550
|
+
navAidsCount++;
|
|
551
|
+
if (navPatterns.skipLink)
|
|
552
|
+
navAidsCount++;
|
|
553
|
+
summary.navigationAidsCount = navAidsCount;
|
|
554
|
+
// Report missing breadcrumbs (only for multi-page sites with depth)
|
|
555
|
+
if (!navPatterns.breadcrumbs) {
|
|
556
|
+
issues.push({
|
|
557
|
+
category: "findability",
|
|
558
|
+
severity: "low",
|
|
559
|
+
subcategory: "navigation-patterns",
|
|
560
|
+
element: "nav",
|
|
561
|
+
description: "No breadcrumb navigation found",
|
|
562
|
+
detectionMethod: "breadcrumb-check",
|
|
563
|
+
recommendation: "Add breadcrumb navigation for hierarchical sites",
|
|
564
|
+
codeExample: `<nav aria-label="Breadcrumb">
|
|
565
|
+
<ol>
|
|
566
|
+
<li><a href="/">Home</a></li>
|
|
567
|
+
<li><a href="/section">Section</a></li>
|
|
568
|
+
<li aria-current="page">Current Page</li>
|
|
569
|
+
</ol>
|
|
570
|
+
</nav>`,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
// Report missing skip link
|
|
574
|
+
if (!navPatterns.skipLink) {
|
|
575
|
+
issues.push({
|
|
576
|
+
category: "accessibility",
|
|
577
|
+
severity: "medium",
|
|
578
|
+
subcategory: "navigation-patterns",
|
|
579
|
+
element: "body",
|
|
580
|
+
description: "No skip-to-content link found",
|
|
581
|
+
detectionMethod: "skip-link-check",
|
|
582
|
+
recommendation: "Add a skip link for keyboard navigation",
|
|
583
|
+
codeExample: `<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
584
|
+
<!-- CSS: .skip-link { position: absolute; left: -10000px; } .skip-link:focus { left: 0; } -->`,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
// Report heading hierarchy issues
|
|
588
|
+
if (!navPatterns.headings.valid) {
|
|
589
|
+
issues.push({
|
|
590
|
+
category: "semantics",
|
|
591
|
+
severity: "medium",
|
|
592
|
+
subcategory: "navigation-patterns",
|
|
593
|
+
element: "h1-h6",
|
|
594
|
+
description: `Heading hierarchy issue: ${navPatterns.headings.issue}`,
|
|
595
|
+
detectionMethod: "heading-hierarchy-check",
|
|
596
|
+
recommendation: "Use a single H1 and maintain proper heading order (h1 → h2 → h3)",
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
// Report missing or short page title
|
|
600
|
+
if (!navPatterns.pageTitle.present) {
|
|
601
|
+
issues.push({
|
|
602
|
+
category: "findability",
|
|
603
|
+
severity: "high",
|
|
604
|
+
subcategory: "navigation-patterns",
|
|
605
|
+
element: "title",
|
|
606
|
+
description: "Page has no title",
|
|
607
|
+
detectionMethod: "page-title-check",
|
|
608
|
+
recommendation: "Add a descriptive <title> element",
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
else if (navPatterns.pageTitle.length < 10) {
|
|
612
|
+
issues.push({
|
|
613
|
+
category: "findability",
|
|
614
|
+
severity: "low",
|
|
615
|
+
subcategory: "navigation-patterns",
|
|
616
|
+
element: "title",
|
|
617
|
+
description: "Page title is very short (< 10 chars)",
|
|
618
|
+
detectionMethod: "page-title-check",
|
|
619
|
+
recommendation: "Use a more descriptive page title",
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Detect actionable elements (action verbs on buttons, aria-describedby)
|
|
625
|
+
* @since 17.0.0
|
|
626
|
+
*/
|
|
627
|
+
async function detectActionableElements(ctx) {
|
|
628
|
+
const { page, issues } = ctx;
|
|
629
|
+
const actionAnalysis = await page.evaluate(() => {
|
|
630
|
+
const buttons = Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]'));
|
|
631
|
+
// Generic/weak button labels
|
|
632
|
+
const genericLabels = ['submit', 'click', 'ok', 'yes', 'no', 'go', 'next', 'back', 'continue', 'send', 'done'];
|
|
633
|
+
// Strong action verbs
|
|
634
|
+
const actionVerbs = ['add', 'create', 'save', 'delete', 'remove', 'edit', 'update', 'download', 'upload', 'share', 'copy', 'search', 'filter', 'sort', 'buy', 'subscribe', 'register', 'login', 'logout', 'sign'];
|
|
635
|
+
const weakButtons = [];
|
|
636
|
+
let elementsWithDescribedBy = 0;
|
|
637
|
+
buttons.forEach(btn => {
|
|
638
|
+
const text = (btn.textContent?.trim() || btn.getAttribute('value') || btn.getAttribute('aria-label') || '').toLowerCase();
|
|
639
|
+
const words = text.split(/\s+/);
|
|
640
|
+
// Check if button uses generic label without action verb
|
|
641
|
+
const isGeneric = words.some(w => genericLabels.includes(w)) && !words.some(w => actionVerbs.some(v => w.startsWith(v)));
|
|
642
|
+
if (isGeneric && text.length < 20) {
|
|
643
|
+
weakButtons.push({
|
|
644
|
+
selector: btn.tagName.toLowerCase() + (btn.id ? `#${btn.id}` : ''),
|
|
645
|
+
text: text.slice(0, 30),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
// Track aria-describedby usage
|
|
649
|
+
if (btn.hasAttribute('aria-describedby')) {
|
|
650
|
+
elementsWithDescribedBy++;
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
return {
|
|
654
|
+
weakButtons: weakButtons.slice(0, 5), // Limit to avoid noise
|
|
655
|
+
totalButtons: buttons.length,
|
|
656
|
+
elementsWithDescribedBy,
|
|
657
|
+
};
|
|
658
|
+
});
|
|
659
|
+
// Report buttons with generic labels
|
|
660
|
+
for (const btn of actionAnalysis.weakButtons) {
|
|
661
|
+
issues.push({
|
|
662
|
+
category: "findability",
|
|
663
|
+
severity: "low",
|
|
664
|
+
subcategory: "actionable-elements",
|
|
665
|
+
element: btn.selector,
|
|
666
|
+
description: `Button with generic label: "${btn.text}"`,
|
|
667
|
+
detectionMethod: "action-verb-check",
|
|
668
|
+
recommendation: "Use specific action verbs (e.g., 'Save Changes' instead of 'Submit')",
|
|
669
|
+
codeExample: `<!-- Instead of: -->
|
|
670
|
+
<button>Submit</button>
|
|
671
|
+
<!-- Use: -->
|
|
672
|
+
<button>Save Changes</button>
|
|
673
|
+
<button>Create Account</button>
|
|
674
|
+
<button>Download Report</button>`,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Detect content-to-navigation ratio
|
|
680
|
+
* @since 17.0.0
|
|
681
|
+
*/
|
|
682
|
+
async function detectContentChrome(ctx) {
|
|
683
|
+
const { page, issues } = ctx;
|
|
684
|
+
const ratio = await page.evaluate(() => {
|
|
685
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
686
|
+
const viewportArea = viewport.width * viewport.height;
|
|
687
|
+
// Calculate nav/header/footer area
|
|
688
|
+
let chromeArea = 0;
|
|
689
|
+
const chromeElements = document.querySelectorAll('nav, header, footer, aside, [role="navigation"], [role="banner"], [role="contentinfo"], [role="complementary"]');
|
|
690
|
+
chromeElements.forEach(el => {
|
|
691
|
+
const rect = el.getBoundingClientRect();
|
|
692
|
+
// Only count visible elements in viewport
|
|
693
|
+
if (rect.width > 0 && rect.height > 0 && rect.top < viewport.height) {
|
|
694
|
+
const visibleHeight = Math.min(rect.bottom, viewport.height) - Math.max(rect.top, 0);
|
|
695
|
+
const visibleWidth = Math.min(rect.right, viewport.width) - Math.max(rect.left, 0);
|
|
696
|
+
if (visibleHeight > 0 && visibleWidth > 0) {
|
|
697
|
+
chromeArea += visibleWidth * visibleHeight;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
// Calculate main content area
|
|
702
|
+
let mainArea = 0;
|
|
703
|
+
const mainEl = document.querySelector('main, [role="main"], article, .content, #content, #main');
|
|
704
|
+
if (mainEl) {
|
|
705
|
+
const rect = mainEl.getBoundingClientRect();
|
|
706
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
707
|
+
const visibleHeight = Math.min(rect.bottom, viewport.height) - Math.max(rect.top, 0);
|
|
708
|
+
const visibleWidth = Math.min(rect.right, viewport.width) - Math.max(rect.left, 0);
|
|
709
|
+
if (visibleHeight > 0 && visibleWidth > 0) {
|
|
710
|
+
mainArea = visibleWidth * visibleHeight;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const chromeRatio = chromeArea / viewportArea;
|
|
715
|
+
const contentRatio = mainArea / viewportArea;
|
|
716
|
+
return {
|
|
717
|
+
chromeRatio: Math.round(chromeRatio * 100),
|
|
718
|
+
contentRatio: Math.round(contentRatio * 100),
|
|
719
|
+
viewportArea,
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
// Flag excessive chrome (> 50% of viewport)
|
|
723
|
+
if (ratio.chromeRatio > 50) {
|
|
724
|
+
issues.push({
|
|
725
|
+
category: "findability",
|
|
726
|
+
severity: "medium",
|
|
727
|
+
subcategory: "content-chrome",
|
|
728
|
+
element: "viewport",
|
|
729
|
+
description: `Navigation/chrome occupies ${ratio.chromeRatio}% of above-fold viewport`,
|
|
730
|
+
detectionMethod: "content-chrome-check",
|
|
731
|
+
recommendation: "Reduce navigation prominence or use collapsible menus",
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Detect API exposure (links to /api/ endpoints, GraphQL hints)
|
|
737
|
+
* @since 17.0.0
|
|
738
|
+
*/
|
|
739
|
+
async function detectApiExposure(ctx) {
|
|
740
|
+
const { page, issues, summary } = ctx;
|
|
741
|
+
const apiInfo = await page.evaluate(() => {
|
|
742
|
+
const pageSource = document.documentElement.outerHTML;
|
|
743
|
+
// Look for /api/ patterns in links and scripts
|
|
744
|
+
const apiPatterns = pageSource.match(/["'](\/api\/[^"']*|https?:\/\/[^"']*\/api\/[^"']*)["']/gi) || [];
|
|
745
|
+
const uniqueApiPaths = [...new Set(apiPatterns.map(p => p.replace(/["']/g, '').split('?')[0]))];
|
|
746
|
+
// Look for GraphQL hints
|
|
747
|
+
const hasGraphQL = /graphql|__schema|__typename/i.test(pageSource);
|
|
748
|
+
const graphqlEndpoint = pageSource.match(/["'](\/graphql|https?:\/\/[^"']*graphql[^"']*)["']/i);
|
|
749
|
+
return {
|
|
750
|
+
apiEndpoints: uniqueApiPaths.slice(0, 10),
|
|
751
|
+
hasGraphQL,
|
|
752
|
+
graphqlEndpoint: graphqlEndpoint ? graphqlEndpoint[1] : null,
|
|
753
|
+
};
|
|
754
|
+
});
|
|
755
|
+
summary.apiEndpointsCount = apiInfo.apiEndpoints.length;
|
|
756
|
+
// Report API exposure as informational (positive for agents)
|
|
757
|
+
if (apiInfo.apiEndpoints.length > 0) {
|
|
758
|
+
issues.push({
|
|
759
|
+
category: "findability",
|
|
760
|
+
severity: "low", // Actually positive - just noting for awareness
|
|
761
|
+
subcategory: "api-exposure",
|
|
762
|
+
element: "script/link",
|
|
763
|
+
description: `${apiInfo.apiEndpoints.length} API endpoint(s) detected in page source`,
|
|
764
|
+
detectionMethod: "api-exposure-check",
|
|
765
|
+
recommendation: "API endpoints are agent-friendly. Ensure they're documented.",
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (apiInfo.hasGraphQL) {
|
|
769
|
+
issues.push({
|
|
770
|
+
category: "findability",
|
|
771
|
+
severity: "low",
|
|
772
|
+
subcategory: "api-exposure",
|
|
773
|
+
element: apiInfo.graphqlEndpoint || "script",
|
|
774
|
+
description: "GraphQL API detected",
|
|
775
|
+
detectionMethod: "graphql-check",
|
|
776
|
+
recommendation: "GraphQL is agent-friendly. Consider enabling introspection for agents.",
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Detect /llms.txt presence
|
|
782
|
+
* @since 17.0.0
|
|
783
|
+
*/
|
|
784
|
+
async function detectLlmsTxt(ctx) {
|
|
785
|
+
const { page, issues, summary } = ctx;
|
|
786
|
+
try {
|
|
787
|
+
const pageUrl = new URL(page.url());
|
|
788
|
+
const llmsTxtUrl = `${pageUrl.origin}/llms.txt`;
|
|
789
|
+
// Use page context to fetch (same cookies/auth)
|
|
790
|
+
const response = await page.context().request.get(llmsTxtUrl, {
|
|
791
|
+
timeout: 5000,
|
|
792
|
+
failOnStatusCode: false,
|
|
793
|
+
});
|
|
794
|
+
const status = response.status();
|
|
795
|
+
summary.hasLlmsTxt = status === 200;
|
|
796
|
+
if (status === 200) {
|
|
797
|
+
const content = await response.text();
|
|
798
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
799
|
+
// Basic validation
|
|
800
|
+
const hasTitle = lines.some(l => l.startsWith('#'));
|
|
801
|
+
const hasLinks = lines.some(l => l.includes('[') && l.includes(']('));
|
|
802
|
+
if (!hasTitle || !hasLinks) {
|
|
803
|
+
issues.push({
|
|
804
|
+
category: "semantics",
|
|
805
|
+
severity: "low",
|
|
806
|
+
subcategory: "llms-txt",
|
|
807
|
+
element: "/llms.txt",
|
|
808
|
+
description: "llms.txt exists but may be incomplete (missing headers or links)",
|
|
809
|
+
detectionMethod: "llms-txt-check",
|
|
810
|
+
recommendation: "Add markdown headers (#) and links ([text](url)) to llms.txt",
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
// No issue if llms.txt is present and valid - it's a positive signal
|
|
814
|
+
}
|
|
815
|
+
else if (status === 404) {
|
|
816
|
+
issues.push({
|
|
817
|
+
category: "semantics",
|
|
818
|
+
severity: "low",
|
|
819
|
+
subcategory: "llms-txt",
|
|
820
|
+
element: "/llms.txt",
|
|
821
|
+
description: "No /llms.txt found (recommended for AI agent documentation)",
|
|
822
|
+
detectionMethod: "llms-txt-check",
|
|
823
|
+
recommendation: "Add /llms.txt to help AI agents understand your site",
|
|
824
|
+
codeExample: `# Site Name
|
|
825
|
+
|
|
826
|
+
> Brief description of the site
|
|
827
|
+
|
|
828
|
+
## Documentation
|
|
829
|
+
- [Getting Started](/docs/start)
|
|
830
|
+
- [API Reference](/api)
|
|
831
|
+
|
|
832
|
+
## Important Pages
|
|
833
|
+
- [Pricing](/pricing)
|
|
834
|
+
- [Contact](/contact)`,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
// Silently handle fetch errors - don't penalize for network issues
|
|
840
|
+
summary.hasLlmsTxt = false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Detect state persistence patterns (CSRF tokens, session indicators)
|
|
845
|
+
* @since 17.0.0
|
|
846
|
+
*/
|
|
847
|
+
async function detectStatePersistence(ctx) {
|
|
848
|
+
const { page, issues } = ctx;
|
|
849
|
+
const stateInfo = await page.evaluate(() => {
|
|
850
|
+
// Look for CSRF tokens
|
|
851
|
+
const csrfInputs = document.querySelectorAll('input[name*="csrf" i], input[name*="token" i], input[name="_token"], input[name="authenticity_token"]');
|
|
852
|
+
const csrfMeta = document.querySelector('meta[name*="csrf" i]');
|
|
853
|
+
const hasCsrf = csrfInputs.length > 0 || !!csrfMeta;
|
|
854
|
+
// Look for session indicators in forms
|
|
855
|
+
const hiddenInputs = document.querySelectorAll('input[type="hidden"]');
|
|
856
|
+
const sessionIndicators = Array.from(hiddenInputs).filter(input => {
|
|
857
|
+
const name = input.getAttribute('name')?.toLowerCase() || '';
|
|
858
|
+
return name.includes('session') || name.includes('state') || name.includes('nonce');
|
|
859
|
+
});
|
|
860
|
+
// Look for forms that might have non-idempotent actions
|
|
861
|
+
const forms = document.querySelectorAll('form');
|
|
862
|
+
const postForms = Array.from(forms).filter(f => f.method.toLowerCase() === 'post');
|
|
863
|
+
return {
|
|
864
|
+
hasCsrf,
|
|
865
|
+
csrfCount: csrfInputs.length + (csrfMeta ? 1 : 0),
|
|
866
|
+
sessionIndicators: sessionIndicators.length,
|
|
867
|
+
postFormCount: postForms.length,
|
|
868
|
+
};
|
|
869
|
+
});
|
|
870
|
+
// Report CSRF presence as informational (awareness for agents)
|
|
871
|
+
if (stateInfo.hasCsrf) {
|
|
872
|
+
issues.push({
|
|
873
|
+
category: "stability",
|
|
874
|
+
severity: "low",
|
|
875
|
+
subcategory: "state-persistence",
|
|
876
|
+
element: "form",
|
|
877
|
+
description: `${stateInfo.csrfCount} CSRF token(s) detected - agents need fresh tokens per request`,
|
|
878
|
+
detectionMethod: "csrf-check",
|
|
879
|
+
recommendation: "Ensure API endpoints support token refresh or use stateless auth for agents",
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
if (stateInfo.sessionIndicators > 0) {
|
|
883
|
+
issues.push({
|
|
884
|
+
category: "stability",
|
|
885
|
+
severity: "low",
|
|
886
|
+
subcategory: "state-persistence",
|
|
887
|
+
element: "input[type='hidden']",
|
|
888
|
+
description: `${stateInfo.sessionIndicators} session state indicator(s) in forms`,
|
|
889
|
+
detectionMethod: "session-check",
|
|
890
|
+
recommendation: "Document required session state for programmatic form submission",
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Detect dynamic content patterns (loading states, infinite scroll, lazy load)
|
|
896
|
+
* @since 17.0.0
|
|
897
|
+
*/
|
|
898
|
+
async function detectDynamicContent(ctx) {
|
|
899
|
+
const { page, issues, summary } = ctx;
|
|
900
|
+
const dynamicInfo = await page.evaluate(() => {
|
|
901
|
+
// Look for loading indicators
|
|
902
|
+
const loadingIndicators = document.querySelectorAll('[class*="loading" i], [class*="spinner" i], [class*="skeleton" i], [aria-busy="true"], [data-loading]');
|
|
903
|
+
// Look for infinite scroll patterns
|
|
904
|
+
const infiniteScrollHints = document.querySelectorAll('[data-infinite], [class*="infinite" i], [class*="load-more" i]');
|
|
905
|
+
// Look for lazy-load images
|
|
906
|
+
const lazyImages = document.querySelectorAll('img[loading="lazy"], img[data-src], img[data-lazy]');
|
|
907
|
+
// Check for intersection observer usage (common for infinite scroll)
|
|
908
|
+
const hasIntersectionObserver = typeof IntersectionObserver !== 'undefined';
|
|
909
|
+
// Look for "Load More" buttons
|
|
910
|
+
const loadMoreButtons = Array.from(document.querySelectorAll('button, a')).filter(el => {
|
|
911
|
+
const text = el.textContent?.toLowerCase() || '';
|
|
912
|
+
return text.includes('load more') || text.includes('show more') || text.includes('view more');
|
|
913
|
+
});
|
|
914
|
+
return {
|
|
915
|
+
loadingIndicators: loadingIndicators.length,
|
|
916
|
+
infiniteScrollHints: infiniteScrollHints.length,
|
|
917
|
+
lazyImages: lazyImages.length,
|
|
918
|
+
loadMoreButtons: loadMoreButtons.length,
|
|
919
|
+
hasIntersectionObserver,
|
|
920
|
+
};
|
|
921
|
+
});
|
|
922
|
+
const dynamicCount = (dynamicInfo.loadingIndicators > 0 ? 1 : 0) +
|
|
923
|
+
(dynamicInfo.infiniteScrollHints > 0 ? 1 : 0) +
|
|
924
|
+
(dynamicInfo.lazyImages > 5 ? 1 : 0) +
|
|
925
|
+
(dynamicInfo.loadMoreButtons > 0 ? 1 : 0);
|
|
926
|
+
summary.dynamicContentCount = dynamicCount;
|
|
927
|
+
// Report infinite scroll as a potential challenge for agents
|
|
928
|
+
if (dynamicInfo.infiniteScrollHints > 0 || dynamicInfo.loadMoreButtons > 0) {
|
|
929
|
+
issues.push({
|
|
930
|
+
category: "stability",
|
|
931
|
+
severity: "medium",
|
|
932
|
+
subcategory: "dynamic-content",
|
|
933
|
+
element: "body",
|
|
934
|
+
description: "Infinite scroll or load-more pattern detected",
|
|
935
|
+
detectionMethod: "infinite-scroll-check",
|
|
936
|
+
recommendation: "Provide pagination alternative or API endpoint for programmatic access",
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
// Report loading indicators
|
|
940
|
+
if (dynamicInfo.loadingIndicators > 3) {
|
|
941
|
+
issues.push({
|
|
942
|
+
category: "stability",
|
|
943
|
+
severity: "low",
|
|
944
|
+
subcategory: "dynamic-content",
|
|
945
|
+
element: "body",
|
|
946
|
+
description: `${dynamicInfo.loadingIndicators} loading indicator(s) found - agents should wait for content`,
|
|
947
|
+
detectionMethod: "loading-state-check",
|
|
948
|
+
recommendation: "Use aria-busy and loading states consistently for better agent detection",
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// ============================================================================
|
|
384
953
|
// Report Generation
|
|
385
954
|
// ============================================================================
|
|
386
955
|
function generateRecommendations(issues) {
|
|
@@ -874,6 +1443,12 @@ export async function runAgentReadyAudit(url, _options = {}) {
|
|
|
874
1443
|
stickyOverlays: 0,
|
|
875
1444
|
customDropdowns: 0,
|
|
876
1445
|
elementsWithoutText: 0,
|
|
1446
|
+
// AI-specific counters (v17.0.0)
|
|
1447
|
+
machineMetadataCount: 0,
|
|
1448
|
+
navigationAidsCount: 0,
|
|
1449
|
+
hasLlmsTxt: false,
|
|
1450
|
+
apiEndpointsCount: 0,
|
|
1451
|
+
dynamicContentCount: 0,
|
|
877
1452
|
};
|
|
878
1453
|
const ctx = { page, issues, summary };
|
|
879
1454
|
// Run all detection functions
|
|
@@ -884,6 +1459,15 @@ export async function runAgentReadyAudit(url, _options = {}) {
|
|
|
884
1459
|
await detectMissingAltText(ctx);
|
|
885
1460
|
await detectBadLinks(ctx);
|
|
886
1461
|
await detectLowFindabilityElements(ctx);
|
|
1462
|
+
// AI-specific detection functions (v17.0.0)
|
|
1463
|
+
await detectMachineMetadata(ctx);
|
|
1464
|
+
await detectNavigationPatterns(ctx);
|
|
1465
|
+
await detectActionableElements(ctx);
|
|
1466
|
+
await detectContentChrome(ctx);
|
|
1467
|
+
await detectApiExposure(ctx);
|
|
1468
|
+
await detectLlmsTxt(ctx);
|
|
1469
|
+
await detectStatePersistence(ctx);
|
|
1470
|
+
await detectDynamicContent(ctx);
|
|
887
1471
|
// Update summary
|
|
888
1472
|
summary.problematicElements = issues.length;
|
|
889
1473
|
// Calculate scores
|