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.