DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to Monitor Website Changes Automatically

How to Monitor Website Changes Automatically

You need to know when a website changes. Your competitor ships a feature. Your site breaks. A third-party integration goes down.

Right now, you're checking manually. Or you've built a Puppeteer scraper that's 400 lines of error handling.

There's a simpler way: screenshot the page on a cron, compare with ImageMagick.

The Problem: Manual Change Detection

Typical change monitoring workflow:

  • Set up a cron job
  • Launch Puppeteer browser
  • Take a screenshot
  • Compare pixels with previous screenshot
  • Send alert on difference
  • Manage browser lifecycle

Issues:

  • 300MB+ Puppeteer dependency
  • Browser launch overhead per check
  • Image comparison is complex (pixel drift, anti-aliasing)
  • Fails in serverless / small VPS
  • Maintenance burden on cron infrastructure

The Solution: PageBolt + ImageMagick

One API call per screenshot. Zero browser management. Compare with standard CLI tools.

# Install ImageMagick
apt-get install imagemagick

# Create monitoring directory
mkdir -p screenshots comparisons

# Set your API key
export PAGEBOLT_API_KEY=your_api_key
Enter fullscreen mode Exit fullscreen mode

Node.js Website Monitoring

Monitor a single website for changes:

const fs = require('fs');
const { spawn } = require('child_process');
const apiKey = process.env.PAGEBOLT_API_KEY;

async function captureAndCompare(url, siteName) {
  const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
  const filename = `screenshots/${siteName}-${timestamp}.png`;
  const previousDir = `screenshots`;

  // Capture current state
  const response = await fetch('https://api.pagebolt.dev/v1/screenshot', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      url: url,
      format: 'png',
      width: 1280,
      height: 720
    })
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  const buffer = await response.arrayBuffer();
  fs.writeFileSync(filename, Buffer.from(buffer));
  console.log(`✓ Captured: ${filename}`);

  // Find previous screenshot
  const files = fs.readdirSync(previousDir)
    .filter(f => f.startsWith(siteName))
    .sort()
    .reverse();

  if (files.length < 2) {
    console.log('First capture. No comparison baseline yet.');
    return null;
  }

  const previousFile = `${previousDir}/${files[1]}`;

  // Compare with ImageMagick
  return new Promise((resolve) => {
    const compare = spawn('compare', [
      '-metric', 'mae',
      previousFile,
      filename,
      'null:'
    ]);

    let diffPercentage = '';
    compare.stderr.on('data', (data) => {
      diffPercentage += data.toString();
    });

    compare.on('close', () => {
      // Parse MAE (Mean Absolute Error) from ImageMagick output
      const mae = parseFloat(diffPercentage);
      const threshold = 1000; // Adjust based on sensitivity

      if (mae > threshold) {
        console.log(`🚨 CHANGE DETECTED: ${mae.toFixed(0)} MAE difference`);
        console.log(`   Previous: ${previousFile}`);
        console.log(`   Current:  ${filename}`);
        resolve({ changed: true, mae, previousFile, currentFile: filename });
      } else {
        console.log(`✓ No significant change (MAE: ${mae.toFixed(0)})`);
        resolve({ changed: false, mae });
      }
    });
  });
}

// Usage with cron
captureAndCompare('https://example.com', 'example-com')
  .then(result => {
    if (result?.changed) {
      // Send alert (email, Slack, webhook)
      console.log('Alert triggered. Send notification to team.');
    }
  })
  .catch(err => console.error('Monitor error:', err.message));
Enter fullscreen mode Exit fullscreen mode

Cron Setup

Monitor multiple sites every hour:

#!/bin/bash
# /usr/local/bin/monitor-sites.sh

export PAGEBOLT_API_KEY=your_api_key
export NODE_ENV=production

cd /home/monitor-app

# Monitor multiple sites
node monitor.js --url https://example.com --name example-com
node monitor.js --url https://github.com --name github-com
node monitor.js --url https://news.ycombinator.com --name hacker-news
Enter fullscreen mode Exit fullscreen mode

Crontab entry (run every hour):

0 * * * * /usr/local/bin/monitor-sites.sh >> /var/log/monitor.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Advanced: Batch Monitoring

Monitor 10 sites in parallel:

const sites = [
  { url: 'https://example.com', name: 'example-com' },
  { url: 'https://github.com', name: 'github' },
  { url: 'https://stackoverflow.com', name: 'stackoverflow' },
  // ... more sites
];

async function monitorAll() {
  const results = await Promise.all(
    sites.map(site => captureAndCompare(site.url, site.name))
  );

  const changed = results.filter(r => r?.changed);
  if (changed.length > 0) {
    console.log(`${changed.length} sites changed today`);
    // Send summary alert
  }
}

monitorAll().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Real-World Use Cases

Competitor Tracking — Know when competitors ship features:

const competitors = [
  'https://stripe.com',
  'https://segment.com',
  'https://amplitude.com'
];

competitors.forEach(url => {
  captureAndCompare(url, url.replace(/\//g, '-'))
    .then(result => {
      if (result?.changed) {
        // Log competitor changes to database
        db.insert('competitor_changes', {
          competitor: url,
          timestamp: new Date(),
          previous: result.previousFile,
          current: result.currentFile
        });
      }
    });
});
Enter fullscreen mode Exit fullscreen mode

Uptime & Health Monitoring — Alert on visual errors:

async function healthCheck(url, siteName) {
  const result = await captureAndCompare(url, siteName);

  if (result?.mae > 5000) {
    // Likely a major change (error page, maintenance, etc)
    sendSlackAlert(`⚠️ ${siteName} may be down. MAE: ${result.mae}`);
  }
}

// Check every 15 minutes
setInterval(() => healthCheck('https://myapp.com', 'myapp'), 15 * 60 * 1000);
Enter fullscreen mode Exit fullscreen mode

Design Regression Testing — Catch unintended style changes:

async function regressionTest(url, branch) {
  const screenshot = await capture(url);

  // Compare with golden baseline stored in git
  const baseline = fs.readFileSync(`baselines/${branch}.png`);
  const diff = await compareImages(baseline, screenshot);

  if (diff.pixels > 1000) {
    console.log('❌ Design regression detected');
    process.exit(1); // Fail CI
  }
}
Enter fullscreen mode Exit fullscreen mode

Pricing

Plan Requests/Month Cost Best For
Free 100 $0 Testing & small sites
Starter 5,000 $29 5-10 sites monitored hourly
Growth 25,000 $79 20+ sites or high frequency
Scale 100,000 $199 Enterprise monitoring networks

Image Comparison Tips

Sensitivity tuning:

  • MAE > 10,000 = major visual change
  • MAE 2,000–5,000 = moderate change
  • MAE < 500 = noise (anti-aliasing, sub-pixel rendering)

Reduce false positives:

# Apply threshold before compare to ignore minor changes
convert previous.png -threshold 50% previous-threshold.png
convert current.png -threshold 50% current-threshold.png
compare -metric mae previous-threshold.png current-threshold.png null:
Enter fullscreen mode Exit fullscreen mode

Store diffs for review:

compare previous.png current.png difference.png
# Creates visual diff showing what changed
Enter fullscreen mode Exit fullscreen mode

Summary

Website change monitoring:

  • ✅ Screenshot API (no browser management)
  • ✅ ImageMagick for comparison (installed on any Linux)
  • ✅ Cron jobs for scheduling (built-in on servers)
  • ✅ Timestamped history (easy debugging)
  • ✅ Scalable to 100+ sites

Get started: Try PageBolt free — 100 requests/month, no credit card required →

Top comments (0)