Back to Blog

Monitor Website Changes with Automated Screenshots

8 min readMarch 14, 2026

Traditional uptime monitoring tells you if your site is returning HTTP 200. But a 200 response does not mean your page looks right. CSS can break, images can disappear, layouts can shift -- and your monitoring will not catch any of it. Visual monitoring with automated screenshots fills this gap.

Why Visual Monitoring?

Here are real scenarios that HTTP monitoring misses but visual monitoring catches:

Architecture Overview

A visual monitoring system has three parts:

  1. Screenshot capture: Take screenshots of your pages on a schedule (hourly, daily).
  2. Comparison: Compare the new screenshot with the previous one to detect changes.
  3. Alerting: Notify you when a change exceeds a threshold (email, Slack, webhook).

Step 1: Scheduled Screenshot Capture

Use a Screenshot API to capture pages on a schedule. Here is a Node.js script that captures multiple pages and saves them with timestamps:

const fs = require('fs');
const path = require('path');

const API_KEY = process.env.SCREENSHOT_API_KEY;
const API_BASE = 'https://screenshotapi-api-production.up.railway.app';

const PAGES_TO_MONITOR = [
  { name: 'homepage', url: 'https://yoursite.com' },
  { name: 'pricing', url: 'https://yoursite.com/pricing' },
  { name: 'signup', url: 'https://yoursite.com/signup' },
  { name: 'dashboard', url: 'https://yoursite.com/dashboard' },
];

async function captureScreenshot(url, options = {}) {
  const params = new URLSearchParams({
    url,
    format: 'png',
    width: String(options.width || 1920),
    height: String(options.height || 1080),
    fullpage: 'true',
    blockads: 'true', // Remove cookie banners for consistent comparisons
  });

  const response = await fetch(`${API_BASE}/v1/screenshot?${params}`, {
    headers: { 'Authorization': `Bearer ${API_KEY}` },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error || `HTTP ${response.status}`);
  }

  return Buffer.from(await response.arrayBuffer());
}

async function runCapture() {
  const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
  const outputDir = path.join('screenshots', timestamp);
  fs.mkdirSync(outputDir, { recursive: true });

  console.log(`[\x1b[36m${timestamp}\x1b[0m] Starting capture run...`);

  for (const page of PAGES_TO_MONITOR) {
    try {
      const buffer = await captureScreenshot(page.url);
      const filePath = path.join(outputDir, `${page.name}.png`);
      fs.writeFileSync(filePath, buffer);
      console.log(`  Captured ${page.name} (${(buffer.length / 1024).toFixed(0)} KB)`);
    } catch (err) {
      console.error(`  FAILED ${page.name}: ${err.message}`);
    }
  }

  console.log(`Capture complete. Files saved to ${outputDir}`);
}

runCapture();

Run this script on a schedule using cron, GitHub Actions, or a task scheduler:

# Cron: capture every hour
0 * * * * cd /path/to/monitor && node capture.js >> monitor.log 2>&1

# Or use node-cron in your app
const cron = require('node-cron');
cron.schedule('0 * * * *', runCapture);

Step 2: Image Comparison

Compare screenshots to detect visual changes. The simplest approach uses pixel-by-pixel comparison with a library like pixelmatch:

const { PNG } = require('pngjs');
const pixelmatch = require('pixelmatch');
const fs = require('fs');

function compareScreenshots(path1, path2, diffPath) {
  const img1 = PNG.sync.read(fs.readFileSync(path1));
  const img2 = PNG.sync.read(fs.readFileSync(path2));

  // Images must be the same size
  if (img1.width !== img2.width || img1.height !== img2.height) {
    return { match: false, reason: 'size_mismatch', diff: 100 };
  }

  const diff = new PNG({ width: img1.width, height: img1.height });

  const mismatchedPixels = pixelmatch(
    img1.data,
    img2.data,
    diff.data,
    img1.width,
    img1.height,
    { threshold: 0.1 } // Color sensitivity (0 = exact, 1 = very lenient)
  );

  const totalPixels = img1.width * img1.height;
  const diffPercentage = (mismatchedPixels / totalPixels) * 100;

  // Save visual diff image
  if (diffPath) {
    fs.writeFileSync(diffPath, PNG.sync.write(diff));
  }

  return {
    match: diffPercentage < 1, // Less than 1% change = match
    diffPercentage: diffPercentage.toFixed(2),
    mismatchedPixels,
    totalPixels,
  };
}

// Compare today vs yesterday
const result = compareScreenshots(
  'screenshots/2026-03-14/homepage.png',
  'screenshots/2026-03-13/homepage.png',
  'screenshots/diffs/homepage-diff.png'
);

if (!result.match) {
  console.log(`CHANGE DETECTED: ${result.diffPercentage}% of pixels changed`);
  // Send alert...
} else {
  console.log('No significant changes detected');
}

Step 3: Alerting

When a change exceeds your threshold, send an alert. Here is a simple Slack notification:

async function sendSlackAlert(pageName, diffPercentage, diffImagePath) {
  const webhookUrl = process.env.SLACK_WEBHOOK_URL;

  await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `Visual change detected on *${pageName}*`,
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*Visual Change Alert*\n` +
              `Page: ${pageName}\n` +
              `Change: ${diffPercentage}% of pixels\n` +
              `Time: ${new Date().toISOString()}`,
          },
        },
      ],
    }),
  });
}

// Use in your comparison flow
if (!result.match) {
  await sendSlackAlert('Homepage', result.diffPercentage);
}

Multi-Viewport Monitoring

Monitor your site across different viewports to catch responsive design issues:

const VIEWPORTS = [
  { name: 'desktop', width: 1920, height: 1080 },
  { name: 'tablet', width: 768, height: 1024 },
  { name: 'mobile', width: 375, height: 812 },
];

for (const viewport of VIEWPORTS) {
  for (const page of PAGES_TO_MONITOR) {
    const buffer = await captureScreenshot(page.url, viewport);
    const filename = `${page.name}-${viewport.name}.png`;
    fs.writeFileSync(path.join(outputDir, filename), buffer);
  }
}

Use Case: Competitor Monitoring

Track when competitors change their pricing or landing pages:

const COMPETITOR_PAGES = [
  { name: 'competitor-a-pricing', url: 'https://competitor-a.com/pricing' },
  { name: 'competitor-b-features', url: 'https://competitor-b.com/features' },
  { name: 'competitor-c-homepage', url: 'https://competitor-c.com' },
];

// Run daily and compare with previous day
// Great for: price tracking, feature launches, marketing changes

Complete GitHub Actions Workflow

Here is a ready-to-use GitHub Actions workflow that runs visual monitoring every hour:

# .github/workflows/visual-monitor.yml
name: Visual Monitoring

on:
  schedule:
    - cron: '0 * * * *'  # Every hour
  workflow_dispatch:       # Manual trigger

jobs:
  monitor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci

      - name: Capture screenshots
        run: node scripts/capture.js
        env:
          SCREENSHOT_API_KEY: ${{ secrets.SCREENSHOT_API_KEY }}

      - name: Compare with baseline
        run: node scripts/compare.js
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

      - name: Upload screenshots
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: screenshots-${{ github.run_id }}
          path: screenshots/
          retention-days: 30

Best Practices

  1. Use blockads=true for consistency. Cookie banners and ads change frequently and create false positives. Block them for more reliable comparisons.
  2. Set a reasonable threshold. Start with 1-2% pixel difference. Too sensitive and you get noise from dynamic content (timestamps, ads). Too lenient and you miss real issues.
  3. Ignore dynamic regions. If parts of your page have live data (stock prices, feeds), mask those regions before comparison.
  4. Store baselines in version control. Keep your baseline screenshots in git so you can see the history of visual changes alongside code changes.
  5. Monitor critical paths only. Do not monitor every page. Focus on: homepage, pricing, signup, checkout, and dashboard.

Frequently Asked Questions

How many screenshots do I need for monitoring?

A typical setup monitors 5-10 pages across 3 viewports, captured hourly. That is 360-720 screenshots/month -- well within the free tier of 100/month for daily monitoring or the Pro tier for hourly.

Can I compare screenshots from different viewport sizes?

No -- images must be the same dimensions for pixel comparison. Always use consistent viewport settings for a given monitoring target.

What about visual regression testing in CI/CD?

This same approach works in CI. Capture screenshots of your staging environment after each deploy and compare with the production baseline. Fail the pipeline if changes exceed your threshold.

Related Articles

Start monitoring with ScreenshotAPI

100 free screenshots per month. Perfect for daily monitoring of your key pages.