Monitoring Node.js Alerts April 5, 2026

Website Change Detection with Node.js (2026)

Monitor pages for content, visual, and structural changes. Build a watcher with diff algorithms, screenshot comparison, and automated alerts.

Three Approaches to Change Detection

Website changes fall into three categories, each requiring different detection methods: content changes (text updates, new items), visual changes (layout shifts, color changes, broken images), and structural changes (DOM mutations, new elements, removed sections). A robust monitoring system combines at least two approaches.

Content-Based Detection

The simplest approach: fetch the page, extract text content, compare with the previous version using a diff algorithm.

const crypto = require('crypto');
const { diffLines } = require('diff');
const Database = require('better-sqlite3');

const db = new Database('monitor.db');
db.exec(`CREATE TABLE IF NOT EXISTS snapshots (
  url TEXT, hash TEXT, content TEXT, checked_at TEXT,
  UNIQUE(url, hash)
)`);

async function checkForChanges(url, fetchFn) {
  const html = await fetchFn(url);

  // Extract meaningful text (strip nav, footer, scripts)
  const text = extractMainContent(html);
  const hash = crypto.createHash('md5').update(text).digest('hex');

  const last = db.prepare(
    'SELECT content, hash FROM snapshots WHERE url = ? ORDER BY checked_at DESC LIMIT 1'
  ).get(url);

  if (!last) {
    // First check — store baseline
    db.prepare('INSERT INTO snapshots VALUES (?, ?, ?, ?)').run(url, hash, text, new Date().toISOString());
    return { changed: false, firstCheck: true };
  }

  if (last.hash === hash) return { changed: false };

  // Content changed — compute diff
  const changes = diffLines(last.content, text);
  const added = changes.filter(c => c.added).map(c => c.value.trim()).filter(Boolean);
  const removed = changes.filter(c => c.removed).map(c => c.value.trim()).filter(Boolean);

  db.prepare('INSERT INTO snapshots VALUES (?, ?, ?, ?)').run(url, hash, text, new Date().toISOString());

  return { changed: true, added, removed, url };
}

Visual Change Detection

Content diffs miss visual changes — broken CSS, missing images, layout shifts. Screenshot comparison catches what text-based diffing can't.

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

async function visualDiff(screenshotBuffer, baselinePath) {
  const current = PNG.sync.read(screenshotBuffer);
  
  if (!fs.existsSync(baselinePath)) {
    // Save as baseline
    fs.writeFileSync(baselinePath, screenshotBuffer);
    return { changed: false, firstCheck: true };
  }

  const baseline = PNG.sync.read(fs.readFileSync(baselinePath));
  
  if (current.width !== baseline.width || current.height !== baseline.height) {
    return { changed: true, reason: 'dimensions changed' };
  }

  const diff = new PNG({ width: current.width, height: current.height });
  const mismatchedPixels = pixelmatch(
    baseline.data, current.data, diff.data,
    current.width, current.height,
    { threshold: 0.1 } // 0 = exact, 1 = very loose
  );

  const changePercent = (mismatchedPixels / (current.width * current.height)) * 100;

  if (changePercent > 0.5) { // >0.5% pixels changed
    fs.writeFileSync(baselinePath.replace('.png', '-diff.png'), PNG.sync.write(diff));
    fs.writeFileSync(baselinePath, screenshotBuffer); // Update baseline
    return { changed: true, changePercent: changePercent.toFixed(2), mismatchedPixels };
  }

  return { changed: false, changePercent: changePercent.toFixed(2) };
}

Alert System

Changes are useless without alerts. Send notifications via Slack, email, or webhooks when changes are detected.

const nodemailer = require('nodemailer');

class AlertSystem {
  constructor(config) {
    this.slackWebhook = config.slackWebhook;
    this.emailTransport = nodemailer.createTransport(config.smtp);
    this.emailTo = config.emailTo;
  }

  async alert(change) {
    const message = `🔔 Change detected on ${change.url}\n` +
      `Change: ${change.changePercent || 'content'}%\n` +
      (change.added?.length ? `Added: ${change.added.slice(0, 3).join(', ')}` : '');

    // Slack
    if (this.slackWebhook) {
      await fetch(this.slackWebhook, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          blocks: [
            { type: 'header', text: { type: 'plain_text', text: `Change: ${change.url}` } },
            { type: 'section', text: { type: 'mrkdwn', text: message } }
          ]
        })
      });
    }

    // Email
    if (this.emailTo) {
      await this.emailTransport.sendMail({
        from: 'monitor@yourdomain.com',
        to: this.emailTo,
        subject: `Page changed: ${change.url}`,
        text: message
      });
    }
  }
}

Scheduled Monitoring

Tie it all together with a cron scheduler that runs checks at configurable intervals.

const cron = require('node-cron');

const monitors = [
  { url: 'https://competitor.com/pricing', interval: '0 */6 * * *', type: 'content' },
  { url: 'https://mysite.com', interval: '*/30 * * * *', type: 'visual' },
  { url: 'https://status.provider.com', interval: '*/5 * * * *', type: 'content' }
];

const alerts = new AlertSystem({ slackWebhook: process.env.SLACK_WEBHOOK });

for (const monitor of monitors) {
  cron.schedule(monitor.interval, async () => {
    try {
      const result = monitor.type === 'visual'
        ? await checkVisual(monitor.url)
        : await checkForChanges(monitor.url, fetchRendered);

      if (result.changed) {
        await alerts.alert({ ...result, url: monitor.url });
      }
      console.log(`[${new Date().toISOString()}] ${monitor.url}: ${result.changed ? 'CHANGED' : 'OK'}`);
    } catch (err) {
      console.error(`Monitor failed: ${monitor.url}`, err.message);
    }
  });
  console.log(`Monitoring ${monitor.url} (${monitor.interval})`);
}

SnapAPI for Change Detection

Building the screenshot and HTML fetching infrastructure is the hard part. SnapAPI handles it — capture screenshots for visual comparison and extract structured content for data-level change detection, all via REST.

// Visual monitoring with SnapAPI — no local browser needed
async function checkVisualWithSnapAPI(url) {
  const response = await fetch('https://api.snapapi.pics/v1/screenshot', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Api-Key': process.env.SNAPAPI_KEY },
    body: JSON.stringify({
      url, fullPage: true, format: 'png',
      blockAds: true, blockCookieBanners: true
    })
  });
  const screenshot = Buffer.from(await response.arrayBuffer());
  return visualDiff(screenshot, `./baselines/${encodeURIComponent(url)}.png`);
}

// Content monitoring with SnapAPI /extract — structured change detection
async function checkDataWithSnapAPI(url, schema) {
  const response = await fetch('https://api.snapapi.pics/v1/extract', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Api-Key': process.env.SNAPAPI_KEY },
    body: JSON.stringify({ url, schema })
  });
  const { data } = await response.json();
  const hash = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
  // Compare hash with stored baseline...
  return { data, hash };
}

// Monitor competitor pricing with structured extraction
const pricingChanges = await checkDataWithSnapAPI(
  'https://competitor.com/pricing',
  { plans: [{ name: 'string', price: 'number', features: ['string'] }] }
);

SnapAPI's stealth mode handles bot detection, blockCookieBanners removes overlays that would pollute screenshots, and structured extraction gives you data-level diffs instead of raw HTML. 200 free requests/month.