Allscreenshots Docs
Guides

Website monitoring dashboard

Track visual changes to websites over time

Website monitoring dashboard

Build a system that automatically captures screenshots of websites on a schedule and alerts you to changes.

Use cases

  • Competitor monitoring: Track pricing pages, feature updates, and design changes
  • Compliance: Document website states for regulatory requirements
  • Brand monitoring: Ensure partner sites display your brand correctly
  • Uptime visualization: Visual proof of website availability

Architecture

┌──────────────┐     ┌─────────────────┐     ┌──────────────┐
│  Scheduled   │ --> │  AllScreenshots │ --> │   Webhook    │
│   Capture    │     │      API        │     │   Handler    │
└──────────────┘     └─────────────────┘     └──────────────┘

                                                    v
                                            ┌──────────────┐
                                            │   Database   │
                                            │   Storage    │
                                            └──────────────┘

                                                    v
                                            ┌──────────────┐
                                            │  Dashboard   │
                                            │     UI       │
                                            └──────────────┘

Implementation

Set up scheduled captures

Use the AllScreenshots scheduling API:

// Create schedules for websites to monitor
const websites = [
  {
    name: 'Competitor Pricing',
    url: 'https://competitor.com/pricing',
    schedule: '0 9 * * *', // Daily at 9 AM
  },
  {
    name: 'Partner Homepage',
    url: 'https://partner.com',
    schedule: '0 */4 * * *', // Every 4 hours
  },
  {
    name: 'Our Status Page',
    url: 'https://status.oursite.com',
    schedule: '*/15 * * * *', // Every 15 minutes
  },
];

async function setupMonitoring() {
  for (const site of websites) {
    const response = await fetch('https://api.allscreenshots.com/v1/schedules', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.ALLSCREENSHOTS_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: site.name,
        url: site.url,
        schedule: site.schedule,
        timezone: 'America/New_York',
        options: {
          viewport: { width: 1920, height: 1080 },
          fullPage: true,
          blockAds: true,
          blockCookieBanners: true,
        },
        webhookUrl: 'https://your-app.com/webhooks/screenshot',
        webhookSecret: process.env.WEBHOOK_SECRET,
        retentionDays: 90,
      }),
    });

    const schedule = await response.json();
    console.log(`Created schedule: ${schedule.scheduleId}`);
  }
}

Handle webhook notifications

// Webhook handler
app.post('/webhooks/screenshot', async (req, res) => {
  // Verify webhook signature
  const signature = req.headers['x-allscreenshots-signature'];
  if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const { scheduleId, captureId, result, timestamp } = req.body;

  // Store capture in database
  await db.captures.create({
    scheduleId,
    captureId,
    screenshotUrl: result.url,
    capturedAt: timestamp,
    width: result.width,
    height: result.height,
    size: result.size,
  });

  // Check for changes (optional)
  await detectChanges(scheduleId, captureId);

  res.sendStatus(200);
});

Detect visual changes

Compare new captures with previous ones:

async function detectChanges(scheduleId, captureId) {
  // Get current and previous captures
  const [current, previous] = await db.captures.findMany({
    where: { scheduleId },
    orderBy: { capturedAt: 'desc' },
    take: 2,
  });

  if (!previous) return; // First capture, nothing to compare

  // Download both images
  const currentImage = await fetch(current.screenshotUrl).then(r => r.buffer());
  const previousImage = await fetch(previous.screenshotUrl).then(r => r.buffer());

  // Compare images
  const { diffPercentage, diffImage } = await compareImages(currentImage, previousImage);

  // If significant change detected, alert
  if (diffPercentage > 1) {
    await sendAlert({
      scheduleId,
      captureId,
      diffPercentage,
      diffImage,
      currentUrl: current.screenshotUrl,
      previousUrl: previous.screenshotUrl,
    });
  }
}

Send alerts

async function sendAlert({ scheduleId, diffPercentage, currentUrl, previousUrl }) {
  const schedule = await db.schedules.findUnique({ where: { id: scheduleId } });

  // Send Slack notification
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `Visual change detected on ${schedule.name}`,
      attachments: [
        {
          color: 'warning',
          fields: [
            { title: 'Website', value: schedule.url, short: true },
            { title: 'Change', value: `${diffPercentage.toFixed(2)}%`, short: true },
          ],
          image_url: currentUrl,
        },
      ],
    }),
  });

  // Or send email
  await sendEmail({
    to: '[email protected]',
    subject: `Visual change detected: ${schedule.name}`,
    html: `
      <h2>Visual change detected</h2>
      <p><strong>Website:</strong> ${schedule.url}</p>
      <p><strong>Change:</strong> ${diffPercentage.toFixed(2)}%</p>
      <h3>Current</h3>
      <img src="${currentUrl}" style="max-width: 600px;" />
      <h3>Previous</h3>
      <img src="${previousUrl}" style="max-width: 600px;" />
    `,
  });
}

Build the dashboard

Create a UI to view captures and history:

// Dashboard component
function MonitoringDashboard() {
  const { data: schedules } = useQuery('schedules', fetchSchedules);

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {schedules?.map(schedule => (
        <WebsiteCard key={schedule.id} schedule={schedule} />
      ))}
    </div>
  );
}

function WebsiteCard({ schedule }) {
  const { data: captures } = useQuery(
    ['captures', schedule.id],
    () => fetchCaptures(schedule.id)
  );

  const latestCapture = captures?.[0];

  return (
    <div className="bg-white rounded-lg shadow p-4">
      <h3 className="font-semibold">{schedule.name}</h3>
      <p className="text-sm text-gray-500">{schedule.url}</p>

      {latestCapture && (
        <>
          <img
            src={latestCapture.screenshotUrl}
            alt={schedule.name}
            className="mt-4 rounded border"
          />
          <p className="text-xs text-gray-400 mt-2">
            Last captured: {formatDate(latestCapture.capturedAt)}
          </p>
        </>
      )}

      <button
        onClick={() => openHistory(schedule.id)}
        className="mt-4 text-blue-600 text-sm"
      >
        View history →
      </button>
    </div>
  );
}

Timeline view

Show captures over time with a timeline:

function CaptureTimeline({ scheduleId }) {
  const { data: captures } = useQuery(
    ['captures', scheduleId],
    () => fetchCaptures(scheduleId, { limit: 30 })
  );

  return (
    <div className="flex overflow-x-auto gap-4 p-4">
      {captures?.map((capture, index) => (
        <div key={capture.id} className="flex-shrink-0">
          <img
            src={capture.screenshotUrl}
            alt={`Capture ${index + 1}`}
            className="w-48 h-32 object-cover rounded cursor-pointer hover:ring-2"
            onClick={() => openFullScreen(capture)}
          />
          <p className="text-xs text-center mt-1">
            {formatDate(capture.capturedAt)}
          </p>
        </div>
      ))}
    </div>
  );
}

Side-by-side comparison

function CompareCaptures({ capture1, capture2 }) {
  const [sliderPosition, setSliderPosition] = useState(50);

  return (
    <div className="relative w-full h-96 overflow-hidden">
      {/* Before image (full width) */}
      <img
        src={capture1.screenshotUrl}
        className="absolute inset-0 w-full h-full object-cover"
      />

      {/* After image (clipped) */}
      <div
        className="absolute inset-0 overflow-hidden"
        style={{ width: `${sliderPosition}%` }}
      >
        <img
          src={capture2.screenshotUrl}
          className="w-full h-full object-cover"
          style={{ width: `${100 / (sliderPosition / 100)}%` }}
        />
      </div>

      {/* Slider */}
      <input
        type="range"
        min="0"
        max="100"
        value={sliderPosition}
        onChange={(e) => setSliderPosition(e.target.value)}
        className="absolute bottom-4 left-1/2 transform -translate-x-1/2"
      />
    </div>
  );
}

Best practices

Store screenshot URLs in your database, not the actual images. AllScreenshots handles storage and CDN delivery.

Retention strategy

  • High-frequency monitors: Keep 7-30 days
  • Daily monitors: Keep 90 days
  • Compliance monitors: Keep as required (up to 365 days)

Alert fatigue prevention

  • Set appropriate change thresholds (1-5%)
  • Group similar changes
  • Implement snooze functionality
  • Use digest emails for low-priority monitors

Cost optimization

  • Use appropriate capture frequencies
  • Don't monitor pages that rarely change
  • Use viewport sizes appropriate for your needs
  • Consider thumbnail sizes for dashboard previews

On this page