How to Get a Screenshot in Slack When Your Website Looks Broken
Uptime monitors tell you a page returned 200 — they don't tell you it's a blank white screen. Attach a screenshot to every Slack alert so your team sees exactly what users see.
Uptime monitors like Pingdom and Better Uptime check that your server responds with a 200. They don't check whether the page actually renders correctly — a React app that crashes on load, a blank white screen from a failed API call, or a CSS file that didn't deploy all return HTTP 200.
Here's how to add a screenshot to every alert: when your monitor fires, capture what the page actually looks like and attach it to the Slack message.
The pattern
monitor fires webhook → take screenshot → upload to Slack → post alert with image
Receive the webhook and post with screenshot
import express from "express";
import { WebClient } from "@slack/web-api";
import fs from "fs/promises";
import path from "path";
import os from "os";
const app = express();
app.use(express.json());
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
const ALERT_CHANNEL = process.env.SLACK_ALERT_CHANNEL; // e.g. "#incidents"
app.post("/webhooks/uptime", async (req, res) => {
const { url, status, message } = req.body;
res.json({ ok: true }); // acknowledge immediately
await alertWithScreenshot({ url, status, message });
});
async function alertWithScreenshot({ url, status, message }) {
// 1. Screenshot the broken page
const screenshotRes = await fetch("https://pagebolt.dev/api/v1/screenshot", {
method: "POST",
headers: {
"x-api-key": process.env.PAGEBOLT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
url,
fullPage: false,
blockBanners: true,
}),
});
const imageBuffer = Buffer.from(await screenshotRes.arrayBuffer());
// 2. Upload image to Slack
const tmpFile = path.join(os.tmpdir(), `alert-${Date.now()}.png`);
await fs.writeFile(tmpFile, imageBuffer);
const upload = await slack.filesUploadV2({
channel_id: ALERT_CHANNEL,
file: tmpFile,
filename: "screenshot.png",
initial_comment: [
`🚨 *Site alert* — ${message || `Status: ${status}`}`,
`URL: ${url}`,
`Time: ${new Date().toUTCString()}`,
].join("\n"),
});
await fs.unlink(tmpFile);
console.log(`Alert posted for ${url}: ${upload.ok}`);
}
Stand-alone monitor (no external uptime service)
Run this on a cron to check your own pages and alert if anything looks wrong:
// monitor.js
import cron from "node-cron";
const PAGES = [
{ name: "Home", url: "https://yourapp.com" },
{ name: "Pricing", url: "https://yourapp.com/pricing" },
{ name: "Login", url: "https://yourapp.com/login" },
{ name: "App dashboard", url: "https://app.yourapp.com/dashboard" },
];
// Check every 5 minutes
cron.schedule("*/5 * * * *", async () => {
for (const page of PAGES) {
await checkPage(page);
}
});
async function checkPage({ name, url }) {
try {
const start = Date.now();
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
const ms = Date.now() - start;
if (!res.ok) {
await alertWithScreenshot({
url,
status: res.status,
message: `${name} returned HTTP ${res.status} (${ms}ms)`,
});
return;
}
// Optional: check page content (detect blank screens)
const text = await res.text();
if (text.length < 500 || text.includes("Application Error")) {
await alertWithScreenshot({
url,
status: res.status,
message: `${name} returned 200 but content looks wrong (${text.length} chars)`,
});
}
} catch (err) {
await alertWithScreenshot({
url,
status: 0,
message: `${name} unreachable: ${err.message}`,
});
}
}
Detect visual changes (not just HTTP errors)
Compare the current screenshot to a baseline — alert if it looks different:
import { PNG } from "pngjs";
import pixelmatch from "pixelmatch";
async function checkVisualChange({ name, url, baselinePath }) {
const current = await takeScreenshot(url);
let baseline;
try {
baseline = await fs.readFile(baselinePath);
} catch {
// No baseline yet — save current as baseline
await fs.writeFile(baselinePath, current);
console.log(`Baseline saved for ${name}`);
return;
}
const img1 = PNG.sync.read(baseline);
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 diffPct = (changed / (width * height)) * 100;
if (diffPct > 5) {
// More than 5% of pixels changed
await slack.chat.postMessage({
channel: ALERT_CHANNEL,
text: `⚠️ *Visual change detected* on ${name} (${diffPct.toFixed(1)}% of pixels changed)`,
attachments: [{ text: url }],
});
// Optionally update baseline after alerting
}
}
GitHub Actions — check production after deploy
Add a post-deploy smoke test as the final CI step:
smoke-test:
needs: deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Visual smoke test
env:
PAGEBOLT_API_KEY: ${{ secrets.PAGEBOLT_API_KEY }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_ALERT_CHANNEL: "#deployments"
run: node scripts/smoke-test.js
// scripts/smoke-test.js
const PAGES = ["https://yourapp.com", "https://yourapp.com/pricing"];
for (const url of PAGES) {
const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
method: "POST",
headers: { "x-api-key": process.env.PAGEBOLT_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ url, fullPage: false, blockBanners: true }),
});
const image = Buffer.from(await res.arrayBuffer());
// Post screenshot to Slack as confirmation the deploy looks good
await postToSlack({
text: `✅ Deploy smoke test — ${url}`,
image,
});
}
A successful deploy posts a "looks good" screenshot. A failed deploy posts the broken state. Either way, your team sees exactly what the page looks like the moment the deploy completes.
Get Started Free
100 requests/month, no credit card
Attach screenshots to Slack alerts, run visual smoke tests after every deploy, and detect blank screens that HTTP monitors miss.
Get Your Free API Key →