How to Screenshot Your App on 10 Different Devices in One Script
Checking your app on multiple devices usually means: spinning up iOS Simulator, Android Emulator, resizing your browser, or paying for BrowserStack. None of these run headlessly or fit naturally into a CI pipeline.
Here's a script that captures your app on a full device matrix — phones, tablets, laptops — in parallel, under 60 seconds, with no emulators.
Available devices
// Run this to see the full list of 25+ supported device presets
const res = await fetch("https://pagebolt.dev/api/v1/list-devices", {
headers: { "x-api-key": process.env.PAGEBOLT_API_KEY },
});
const { devices } = await res.json();
console.log(devices.map((d) => d.id).join("\n"));
Key presets:
iphone_14_pro iphone_se
iphone_14_pro_max ipad_pro_12_9
ipad_mini galaxy_s23
galaxy_tab_s8 macbook_pro_16
macbook_air_m2 surface_pro_9
Full device matrix script
import fs from "fs/promises";
import path from "path";
const PAGEBOLT_API_KEY = process.env.PAGEBOLT_API_KEY;
const DEVICES = [
{ id: "iphone_se", label: "iPhone SE (small)" },
{ id: "iphone_14_pro", label: "iPhone 14 Pro" },
{ id: "iphone_14_pro_max", label: "iPhone 14 Pro Max" },
{ id: "ipad_mini", label: "iPad mini" },
{ id: "ipad_pro_12_9", label: "iPad Pro 12.9" },
{ id: "galaxy_s23", label: "Samsung Galaxy S23" },
{ id: "galaxy_tab_s8", label: "Samsung Galaxy Tab S8" },
{ id: "macbook_air_m2", label: "MacBook Air M2" },
{ id: "macbook_pro_16", label: "MacBook Pro 16" },
{ id: "surface_pro_9", label: "Surface Pro 9" },
];
const PAGES = [
{ name: "home", url: "https://yourapp.com" },
{ name: "pricing", url: "https://yourapp.com/pricing" },
{ name: "signin", url: "https://yourapp.com/signin" },
];
async function screenshot(url, deviceId) {
const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
method: "POST",
headers: {
"x-api-key": PAGEBOLT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
url,
viewportDevice: deviceId,
fullPage: false,
blockBanners: true,
}),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
return Buffer.from(await res.arrayBuffer());
}
async function main() {
const outDir = "device-screenshots";
await fs.mkdir(outDir, { recursive: true });
for (const page of PAGES) {
const pageDir = path.join(outDir, page.name);
await fs.mkdir(pageDir, { recursive: true });
console.log(`\n${page.name} — capturing ${DEVICES.length} devices in parallel...`);
const results = await Promise.allSettled(
DEVICES.map(async (device) => {
const image = await screenshot(page.url, device.id);
const filename = `${device.id}.png`;
await fs.writeFile(path.join(pageDir, filename), image);
return { device, filename, bytes: image.length };
})
);
for (const result of results) {
if (result.status === "fulfilled") {
const { device, bytes } = result.value;
console.log(` ✓ ${device.label} (${bytes} bytes)`);
} else {
console.error(` ✗ ${result.reason}`);
}
}
}
console.log(`\nDone. Screenshots in ${outDir}/`);
}
main().catch(console.error);
Output structure:
device-screenshots/
home/
iphone_se.png
iphone_14_pro.png
iphone_14_pro_max.png
ipad_mini.png
ipad_pro_12_9.png
galaxy_s23.png
galaxy_tab_s8.png
macbook_air_m2.png
macbook_pro_16.png
surface_pro_9.png
pricing/
...
signin/
...
Dark mode matrix
const MATRIX = DEVICES.flatMap((device) => [
{ device, darkMode: false, label: `${device.id}-light` },
{ device, darkMode: true, label: `${device.id}-dark` },
]);
await Promise.allSettled(
MATRIX.map(async ({ device, darkMode, label }) => {
const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
method: "POST",
headers: { "x-api-key": PAGEBOLT_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://yourapp.com",
viewportDevice: device.id,
darkMode,
blockBanners: true,
}),
});
const image = Buffer.from(await res.arrayBuffer());
await fs.writeFile(path.join(outDir, `${label}.png`), image);
console.log(`✓ ${label}`);
})
);
GitHub Actions — run on every PR
name: Device matrix screenshots
on:
pull_request:
branches: [main]
jobs:
device-matrix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Wait for preview
run: |
for i in $(seq 1 24); do
[ "$(curl -s -o /dev/null -w '%{http_code}' '${{ vars.PREVIEW_URL }}')" = "200" ] && break
sleep 5
done
- name: Screenshot device matrix
env:
PAGEBOLT_API_KEY: ${{ secrets.PAGEBOLT_API_KEY }}
BASE_URL: ${{ vars.PREVIEW_URL }}
run: node scripts/device-matrix.js
- name: Upload screenshots
uses: actions/upload-artifact@v4
with:
name: device-matrix-${{ github.run_id }}
path: device-screenshots/
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## Device matrix screenshots\n\n10 devices × ${process.env.PAGE_COUNT || 3} pages captured. [Download artifacts](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`
});
Check for responsive breakage automatically
Capture at specific widths to catch layout breakpoints:
const WIDTHS = [320, 375, 768, 1024, 1280, 1440, 1920];
await Promise.allSettled(
WIDTHS.map(async (width) => {
const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
method: "POST",
headers: { "x-api-key": PAGEBOLT_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://yourapp.com",
viewport: { width, height: 900 },
blockBanners: true,
}),
});
const image = Buffer.from(await res.arrayBuffer());
await fs.writeFile(`screenshots/width-${width}.png`, image);
console.log(`✓ ${width}px`);
})
);
10 devices in parallel typically completes in 8–12 seconds. No emulator cold-start, no BrowserStack session limit, no Simulator installation.
Try it free — 100 requests/month, no credit card. → Get started in 2 minutes
Top comments (0)