DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to monitor a website for visual changes (without a browser)

How to Monitor a Website for Visual Changes

Price trackers, competitor monitors, status pages, government data feeds — there's a whole class of problems where you care when a web page looks different, not just when a specific element changes. Parsing HTML is brittle. Scraping structured data works until the layout changes. Screenshots capture everything.

Here's a minimal visual change monitor in Node.js: screenshot a URL, diff against the last capture, alert when the difference exceeds a threshold.

The monitor

import fs from 'fs/promises';
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';

const PAGES = [
  { name: 'competitor-pricing', url: 'https://competitor.com/pricing' },
  { name: 'status-page', url: 'https://status.yourvendor.com' },
];
const THRESHOLD = 0.02; // 2% pixel change triggers alert
const SNAPSHOTS_DIR = './snapshots';

async function capture(url) {
  const res = await fetch('https://api.pagebolt.dev/v1/screenshot', {
    method: 'POST',
    headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ url, fullPage: true, blockBanners: true, blockAds: true })
  });
  return Buffer.from(await res.arrayBuffer());
}

async function check(page) {
  await fs.mkdir(SNAPSHOTS_DIR, { recursive: true });
  const snapshotPath = `${SNAPSHOTS_DIR}/${page.name}.png`;
  const current = await capture(page.url);

  let previous;
  try { previous = await fs.readFile(snapshotPath); }
  catch { await fs.writeFile(snapshotPath, current); console.log(`✓ ${page.name}: baseline saved`); return; }

  const img1 = PNG.sync.read(previous);
  const img2 = PNG.sync.read(current);
  const { width, height } = img1;
  const diff = new PNG({ width, height });
  const changed = pixelmatch(img1.data, img2.data, diff.data, width, height, { threshold: 0.1 });
  const pct = changed / (width * height);

  if (pct > THRESHOLD) {
    await fs.writeFile(`${SNAPSHOTS_DIR}/${page.name}-diff.png`, PNG.sync.write(diff));
    await fs.writeFile(snapshotPath, current); // update baseline
    await alert(page, pct);
  } else {
    console.log(`✓ ${page.name}: no change (${(pct * 100).toFixed(2)}%)`);
  }
}

async function alert(page, pct) {
  console.error(`⚠ CHANGE DETECTED: ${page.name}${(pct * 100).toFixed(1)}% pixels changed`);
  // Add your alerting here: Slack webhook, email, PagerDuty, etc.
  // Example Slack:
  // await fetch(process.env.SLACK_WEBHOOK, {
  //   method: 'POST',
  //   body: JSON.stringify({ text: `Visual change on ${page.name}: ${(pct*100).toFixed(1)}% changed` })
  // });
}

for (const page of PAGES) await check(page);
Enter fullscreen mode Exit fullscreen mode

Run this on a cron — every hour, every 15 minutes, whatever the use case demands. The first run saves a baseline. Every subsequent run diffs against it and updates the snapshot when a significant change is detected.

Run on a schedule (GitHub Actions)

name: Visual Monitor
on:
  schedule:
    - cron: '0 * * * *'  # every hour

jobs:
  monitor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install pixelmatch pngjs
      - run: node monitor.js
        env:
          PAGEBOLT_API_KEY: ${{ secrets.PAGEBOLT_API_KEY }}
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
Enter fullscreen mode Exit fullscreen mode

Store the baseline PNGs as artifacts or commit them to the repo. The diff PNGs upload automatically when changes are detected.


No server. No Puppeteer process. No Selenium grid. A screenshot API call + a pixel diff library + a cron is all you need.

Free tier: 100 requests/month. → pagebolt.dev

Top comments (0)