DEV Community

How We Built an AI Visual CRO Auditor That Draws Bounding Boxes Over UX Friction

The Problem with Traditional Audit Tools

When traditional automated audit tools (like Google Lighthouse) scan a webpage, they test for DOM metrics and performance: Largest Contentful Paint (LCP), missing ARIA labels, image dimensions, or meta tags. What they CANNOT tell you:

  • "Your primary CTA button blends directly into the hero gradient background."
  • "Your pricing table has cognitive overload and 5 competing badge colors."
  • "Your sign-up form creates visual friction by hiding password requirements."

The Solution

To solve this, we built an open-source Visual CRO (Conversion Rate Optimization) engine for Plyxo Community Edition. It captures high-DPI full-page screenshots, feeds them into multimodal vision models, returns normalized coordinate bounding boxes [ymin, xmin, ymax, xmax] over friction zones, and generates copy-paste Tailwind CSS fixes. Here is how the architecture works under the hood.

๐Ÿ—๏ธ The Architecture: From URL to Visual Coordinates

[Target URL]
       โ†“
[Puppeteer / Headless Chrome]
       โ†“
(High-DPI Screenshot + DOM Heuristics)
       โ†“
[Multimodal Vision Model]
       โ†“
(Normalized 0-1000 Coordinates JSON)
       โ†“
[Interactive Canvas Overlay + Tailwind Code Remediation]

1. High-DPI Viewport Rendering & Layout Shifts

Standard screenshots often miss sticky headers, modals, or hydration popups. We use a headless Chrome pipeline that enforces high-DPI rendering and waits for network idle:

// packages/core/src/scanners/screenshot.ts
import puppeteer from 'puppeteer';

export async function captureViewport(url: string) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  const page = await browser.newPage();
  await page.setViewport({
    width: 1440,
    height: 900,
    deviceScaleFactor: 2 // High DPI for crisp font & badge recognition
  });
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });

  // Clean scroll to trigger lazy-loaded sections
  await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight / 2));
  await new Promise(r => setTimeout(r, 600));
  await page.evaluate(() => window.scrollTo(0, 0));

  const screenshotBuffer = await page.screenshot({
    fullPage: false, // Hero/above-the-fold is where 80% of CRO friction happens
    encoding: 'base64'
  });
  await browser.close();
  return screenshotBuffer;
}

2. Prompting Multimodal Vision for Normalized Bounding Boxes

Standard LLMs return chatty explanations. For an interactive UI overlay, we need structured JSON with normalized visual coordinates (0-1000 scale). Here is the system prompt and structured schema we pass to the vision model:

const SYSTEM_PROMPT = `
  You are an expert Conversion Rate Optimization (CRO) and UX Design Auditor.
  Analyze the provided desktop screenshot of a landing page. Identify top UX/CRO friction points:
  1. Contrast/Visibility issues (unclear CTAs)
  2. Visual clutter / Cognitive overload
  3. Lack of immediate value proposition / hierarchy
  4. Trust signal deficiencies

  For each issue, you MUST provide:
  - 'title': Short descriptive title
  - 'severity': 'critical' | 'warning' | 'info'
  - 'box_2d': Normalized coordinates [ymin, xmin, ymax, xmax] between 0 and 1000
  - 'frictionReason': Why this hurts conversion
  - 'proposedCodeFix': Concrete Tailwind CSS / HTML remediation code
`;

3. Rendering the Interactive Canvas Overlay

Once the backend returns the normalized coordinate array, we render dynamic highlight boxes that scale responsively with any container:

// components/VisualCroOverlay.tsx
import React, { useState } from 'react';

interface FrictionBox {
  id: string;
  title: string;
  severity: 'critical' | 'warning' | 'info';
  box_2d: [number, number, number, number]; // [ymin, xmin, ymax, xmax]
  proposedCodeFix: string;
}

export function VisualCroOverlay({ screenshotUrl, issues }: { screenshotUrl: string; issues: FrictionBox[] }) {
  const [selectedIssue, setSelectedIssue] = useState<FrictionBox | null>(null);

  return (
    <div className="relative inline-block w-full border border-slate-800 rounded-xl overflow-hidden shadow-2xl">
      <img src={screenshotUrl} alt="Audited Page" className="w-full h-auto block" />
      {/* Visual Bounding Boxes */}
      {issues.map((issue) => {
        const [ymin, xmin, ymax, xmax] = issue.box_2d;
        const top = `${(ymin / 1000) * 100}%`;
        const left = `${(xmin / 1000) * 100}%`;
        const height = `${((ymax - ymin) / 1000) * 100}%`;
        const width = `${((xmax - xmin) / 1000) * 100}%`;

        const colorMap = {
          critical: 'border-rose-500 bg-rose-500/20 text-rose-300',
          warning: 'border-amber-500 bg-amber-500/20 text-amber-300',
          info: 'border-blue-500 bg-blue-500/20 text-blue-300'
        };

        return (
          <div
            key={issue.id}
            onClick={() => setSelectedIssue(issue)}
            style={{ top, left, height, width }}
            className={`absolute border-2 cursor-pointer transition-all hover:scale-[1.02] ${colorMap[issue.severity]}`}
          >
            <span className="absolute -top-6 left-0 text-xs px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 font-mono">
              {issue.title}
            </span>
          </div>
        );
      })}
    </div>
  );
}

4. Generating Concrete Code Remediation

Instead of just telling the developer "your button lacks contrast", the engine generates the replacement JSX:

Before (Detected Friction):

<button className="bg-indigo-400 text-indigo-100 py-2 px-4 rounded">
  Get Started
</button>

Recommended Fix (High-Contrast Visual Hierarchy + Micro-interaction):

<button className="bg-gradient-to-r from-emerald-500 to-teal-600 text-white font-semibold py-3 px-6 rounded-lg shadow-lg shadow-emerald-500/20 hover:shadow-emerald-500/40 hover:-translate-y-0.5 transition-all duration-200">
  Start Free Audit โ†’
</button>

๐Ÿš€ Try It & Contribute (100% Open Source)

We packaged this visual engine into Plyxo Community Edition, a free, MIT-licensed audit platform that combines:

  • ๐ŸŽจ Visual CRO & Friction Bounding Boxes
  • โšก Core Web Vitals & Real-Time Performance Audits
  • ๐Ÿค– AEO / GEO Engine (Generative Engine Optimization for ChatGPT/Perplexity)
  • ๐Ÿ”Œ Model Context Protocol (MCP) Server for Claude Desktop & Cursor IDE

Quick Start with Docker:

git clone https://github.com/pixelfogg/Plyxo-CRO-SEO-AIO-AEO-GEO.git
cd Plyxo-CRO-SEO-AIO-AEO-GEO
docker compose up -d

Check out the code, run it locally on your own SaaS landing page, or star the repo on GitHub:

  • โญ GitHub Repo: github.com/pixelfogg/Plyxo-CRO-SEO-AIO-AEO-GEO
  • ๐ŸŒ Live Demo: plyxo.org

What techniques are you using to audit landing page conversion rates? Let's discuss in the comments below!

Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.