Guides
Website monitoring dashboard
Track visual changes to websites over time
Website monitoring dashboard
Build a system that automatically captures screenshots of websites on a schedule and alerts you to changes.
Use cases
- Competitor monitoring: Track pricing pages, feature updates, and design changes
- Compliance: Document website states for regulatory requirements
- Brand monitoring: Ensure partner sites display your brand correctly
- Uptime visualization: Visual proof of website availability
Architecture
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
│ Scheduled │ --> │ AllScreenshots │ --> │ Webhook │
│ Capture │ │ API │ │ Handler │
└──────────────┘ └─────────────────┘ └──────────────┘
│
v
┌──────────────┐
│ Database │
│ Storage │
└──────────────┘
│
v
┌──────────────┐
│ Dashboard │
│ UI │
└──────────────┘Implementation
Set up scheduled captures
Use the AllScreenshots scheduling API:
// Create schedules for websites to monitor
const websites = [
{
name: 'Competitor Pricing',
url: 'https://competitor.com/pricing',
schedule: '0 9 * * *', // Daily at 9 AM
},
{
name: 'Partner Homepage',
url: 'https://partner.com',
schedule: '0 */4 * * *', // Every 4 hours
},
{
name: 'Our Status Page',
url: 'https://status.oursite.com',
schedule: '*/15 * * * *', // Every 15 minutes
},
];
async function setupMonitoring() {
for (const site of websites) {
const response = await fetch('https://api.allscreenshots.com/v1/schedules', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ALLSCREENSHOTS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: site.name,
url: site.url,
schedule: site.schedule,
timezone: 'America/New_York',
options: {
viewport: { width: 1920, height: 1080 },
fullPage: true,
blockAds: true,
blockCookieBanners: true,
},
webhookUrl: 'https://your-app.com/webhooks/screenshot',
webhookSecret: process.env.WEBHOOK_SECRET,
retentionDays: 90,
}),
});
const schedule = await response.json();
console.log(`Created schedule: ${schedule.scheduleId}`);
}
}Handle webhook notifications
// Webhook handler
app.post('/webhooks/screenshot', async (req, res) => {
// Verify webhook signature
const signature = req.headers['x-allscreenshots-signature'];
if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const { scheduleId, captureId, result, timestamp } = req.body;
// Store capture in database
await db.captures.create({
scheduleId,
captureId,
screenshotUrl: result.url,
capturedAt: timestamp,
width: result.width,
height: result.height,
size: result.size,
});
// Check for changes (optional)
await detectChanges(scheduleId, captureId);
res.sendStatus(200);
});Detect visual changes
Compare new captures with previous ones:
async function detectChanges(scheduleId, captureId) {
// Get current and previous captures
const [current, previous] = await db.captures.findMany({
where: { scheduleId },
orderBy: { capturedAt: 'desc' },
take: 2,
});
if (!previous) return; // First capture, nothing to compare
// Download both images
const currentImage = await fetch(current.screenshotUrl).then(r => r.buffer());
const previousImage = await fetch(previous.screenshotUrl).then(r => r.buffer());
// Compare images
const { diffPercentage, diffImage } = await compareImages(currentImage, previousImage);
// If significant change detected, alert
if (diffPercentage > 1) {
await sendAlert({
scheduleId,
captureId,
diffPercentage,
diffImage,
currentUrl: current.screenshotUrl,
previousUrl: previous.screenshotUrl,
});
}
}Send alerts
async function sendAlert({ scheduleId, diffPercentage, currentUrl, previousUrl }) {
const schedule = await db.schedules.findUnique({ where: { id: scheduleId } });
// Send Slack notification
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `Visual change detected on ${schedule.name}`,
attachments: [
{
color: 'warning',
fields: [
{ title: 'Website', value: schedule.url, short: true },
{ title: 'Change', value: `${diffPercentage.toFixed(2)}%`, short: true },
],
image_url: currentUrl,
},
],
}),
});
// Or send email
await sendEmail({
to: '[email protected]',
subject: `Visual change detected: ${schedule.name}`,
html: `
<h2>Visual change detected</h2>
<p><strong>Website:</strong> ${schedule.url}</p>
<p><strong>Change:</strong> ${diffPercentage.toFixed(2)}%</p>
<h3>Current</h3>
<img src="${currentUrl}" style="max-width: 600px;" />
<h3>Previous</h3>
<img src="${previousUrl}" style="max-width: 600px;" />
`,
});
}Build the dashboard
Create a UI to view captures and history:
// Dashboard component
function MonitoringDashboard() {
const { data: schedules } = useQuery('schedules', fetchSchedules);
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{schedules?.map(schedule => (
<WebsiteCard key={schedule.id} schedule={schedule} />
))}
</div>
);
}
function WebsiteCard({ schedule }) {
const { data: captures } = useQuery(
['captures', schedule.id],
() => fetchCaptures(schedule.id)
);
const latestCapture = captures?.[0];
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="font-semibold">{schedule.name}</h3>
<p className="text-sm text-gray-500">{schedule.url}</p>
{latestCapture && (
<>
<img
src={latestCapture.screenshotUrl}
alt={schedule.name}
className="mt-4 rounded border"
/>
<p className="text-xs text-gray-400 mt-2">
Last captured: {formatDate(latestCapture.capturedAt)}
</p>
</>
)}
<button
onClick={() => openHistory(schedule.id)}
className="mt-4 text-blue-600 text-sm"
>
View history →
</button>
</div>
);
}Timeline view
Show captures over time with a timeline:
function CaptureTimeline({ scheduleId }) {
const { data: captures } = useQuery(
['captures', scheduleId],
() => fetchCaptures(scheduleId, { limit: 30 })
);
return (
<div className="flex overflow-x-auto gap-4 p-4">
{captures?.map((capture, index) => (
<div key={capture.id} className="flex-shrink-0">
<img
src={capture.screenshotUrl}
alt={`Capture ${index + 1}`}
className="w-48 h-32 object-cover rounded cursor-pointer hover:ring-2"
onClick={() => openFullScreen(capture)}
/>
<p className="text-xs text-center mt-1">
{formatDate(capture.capturedAt)}
</p>
</div>
))}
</div>
);
}Side-by-side comparison
function CompareCaptures({ capture1, capture2 }) {
const [sliderPosition, setSliderPosition] = useState(50);
return (
<div className="relative w-full h-96 overflow-hidden">
{/* Before image (full width) */}
<img
src={capture1.screenshotUrl}
className="absolute inset-0 w-full h-full object-cover"
/>
{/* After image (clipped) */}
<div
className="absolute inset-0 overflow-hidden"
style={{ width: `${sliderPosition}%` }}
>
<img
src={capture2.screenshotUrl}
className="w-full h-full object-cover"
style={{ width: `${100 / (sliderPosition / 100)}%` }}
/>
</div>
{/* Slider */}
<input
type="range"
min="0"
max="100"
value={sliderPosition}
onChange={(e) => setSliderPosition(e.target.value)}
className="absolute bottom-4 left-1/2 transform -translate-x-1/2"
/>
</div>
);
}Best practices
Store screenshot URLs in your database, not the actual images. AllScreenshots handles storage and CDN delivery.
Retention strategy
- High-frequency monitors: Keep 7-30 days
- Daily monitors: Keep 90 days
- Compliance monitors: Keep as required (up to 365 days)
Alert fatigue prevention
- Set appropriate change thresholds (1-5%)
- Group similar changes
- Implement snooze functionality
- Use digest emails for low-priority monitors
Cost optimization
- Use appropriate capture frequencies
- Don't monitor pages that rarely change
- Use viewport sizes appropriate for your needs
- Consider thumbnail sizes for dashboard previews