Monitor Website Changes with Automated Screenshots
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:
- CSS deployment breaks layout: Your deploy succeeds, the page returns 200, but the hero section is now overlapping the navigation.
- Third-party widget fails: A chat widget, ad network, or analytics script throws an error that creates a white box on the page.
- CDN serves stale assets: Images are cached from a previous version, showing outdated branding.
- Responsive breakage: Desktop works fine but mobile viewport is broken.
- Competitor monitoring: Track when competitors change their pricing, features, or landing pages.
Architecture Overview
A visual monitoring system has three parts:
- Screenshot capture: Take screenshots of your pages on a schedule (hourly, daily).
- Comparison: Compare the new screenshot with the previous one to detect changes.
- 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 changesComplete 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: 30Best Practices
- Use blockads=true for consistency. Cookie banners and ads change frequently and create false positives. Block them for more reliable comparisons.
- 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.
- Ignore dynamic regions. If parts of your page have live data (stock prices, feeds), mask those regions before comparison.
- Store baselines in version control. Keep your baseline screenshots in git so you can see the history of visual changes alongside code changes.
- 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.