How to Capture Scheduled Screenshots of a Website at Different Viewport Sizes
Use Playwright and GitHub Actions to capture a website on a schedule at desktop, tablet, and mobile viewport sizes, then archive or compare the images.
To capture a website on a schedule at multiple viewport sizes, write a browser script that sets each viewport, opens the page, waits for a meaningful page condition, and saves a screenshot. Then configure a scheduler such as GitHub Actions to run the script repeatedly and retain the resulting images. The browser script controls what gets captured; the scheduled workflow controls when it runs.
This guide uses Playwright with Node.js and GitHub Actions. It captures the visible viewport by default, with an option for full-page images. You can treat the output as a dated archive or extend the workflow to compare each capture against a baseline.
1. Choose viewport sizes and capture mode
For responsive layout checks, specify explicit CSS viewport width and height values. Choose dimensions that correspond to the breakpoints and layouts you care about; the examples below are illustrative, not universal device standards.
| Purpose | Example viewport | What it represents |
|---|---|---|
| Desktop | 1440 × 900 | A wide browser window |
| Tablet | 768 × 1024 | A narrower, portrait layout |
| Mobile | 390 × 844 | A compact, portrait layout |
A viewport sets the browser’s layout dimensions. A device descriptor can also set properties such as user agent and touch behavior. Use a descriptor when you want device emulation, rather than only testing CSS layout at a particular width and height. See Playwright’s emulation documentation.
Decide what the image should include:
- Viewport screenshot: the visible browser area; useful for checking the initial page view.
- Full-page screenshot: the complete scrollable page; useful for visual archives, though long pages create larger images.
- Element screenshot: a specific matched element, such as a navigation bar or pricing section; useful when the rest of the page is irrelevant.
Playwright documents these screenshot modes and their options in its screenshot guide.
2. Create a Playwright capture script
Install Node.js, then create a project and install Playwright. The commands below install Chromium as the browser used by the script.
mkdir scheduled-site-shots
cd scheduled-site-shots
npm init -y
npm install --save-dev playwright
npx playwright install chromium
Create capture.mjs with the following complete script. It reads the target URL from an environment variable, captures three viewport sizes, waits for a page-specific selector if provided, and creates predictable timestamped filenames. By default it captures the visible viewport. Set FULL_PAGE=true for full-page captures.
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';
const targetUrl = process.env.TARGET_URL;
if (!targetUrl) {
throw new Error('Set TARGET_URL to the page you want to capture.');
}
const waitForSelector = process.env.WAIT_FOR_SELECTOR;
const fullPage = process.env.FULL_PAGE === 'true';
const outputDir = process.env.OUTPUT_DIR || 'screenshots';
const runId = new Date().toISOString().replaceAll(':', '-');
const viewports = [
{ name: 'desktop', width: 1440, height: 900 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'mobile', width: 390, height: 844 },
];
await mkdir(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
for (const viewport of viewports) {
const page = await browser.newPage({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: 1,
});
try {
const response = await page.goto(targetUrl, {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
if (response && !response.ok()) {
throw new Error(`Navigation returned HTTP ${response.status()} for ${targetUrl}`);
}
if (waitForSelector) {
await page.locator(waitForSelector).waitFor({
state: 'visible',
timeout: 30_000,
});
} else {
// A small settling period after DOM readiness; prefer a selector for dynamic pages.
await page.waitForTimeout(1_000);
}
const filename = `${outputDir}/${runId}-${viewport.name}-${viewport.width}x${viewport.height}.png`;
await page.screenshot({ path: filename, fullPage });
console.log(`Saved ${filename}`);
} finally {
await page.close();
}
}
} finally {
await browser.close();
}
Run it locally by setting the URL. On macOS or Linux:
TARGET_URL=https://example.com node capture.mjs
On PowerShell:
$env:TARGET_URL = "https://example.com"
node capture.mjs
For pages that render their main content asynchronously, wait for a stable, meaningful selector rather than relying on the one-second fallback:
TARGET_URL=https://example.com WAIT_FOR_SELECTOR="main h1" node capture.mjs
For a full-page archive:
TARGET_URL=https://example.com FULL_PAGE=true node capture.mjs
3. Schedule recurring captures with GitHub Actions
Commit capture.mjs and package.json to a GitHub repository. Add this workflow at .github/workflows/site-shots.yml. The cron expression below runs daily at 09:00 UTC. GitHub Actions uses UTC for scheduled workflow times; change the cron expression to set the recurrence you need. GitHub documents the schedule event and cron syntax.
name: Scheduled website screenshots
on:
schedule:
- cron: '0 9 * * *'
workflow_dispatch:
jobs:
capture:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
TARGET_URL: https://example.com
WAIT_FOR_SELECTOR: main h1
FULL_PAGE: 'false'
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browser
run: npx playwright install --with-deps chromium
- name: Capture screenshots
run: node capture.mjs
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: website-screenshots-${{ github.run_id }}
path: screenshots/
if-no-files-found: warn
retention-days: 14
Commit the lockfile generated by npm install so npm ci can install the same dependency versions in CI. The artifact step retains images for 14 days in this example; choose a retention period that fits your review needs. Playwright’s CI guide explains browser setup in continuous integration and demonstrates artifact handling.
Scheduled jobs can be delayed during periods of high GitHub Actions load. Treat a schedule as a recurring trigger, not a precise timing guarantee. Use workflow_dispatch to run a capture manually when needed. If missing a capture has operational consequences, add monitoring for successful runs and choose a scheduler and storage arrangement appropriate to that requirement.
4. Capture additional URLs or custom viewport combinations
To add sizes, append entries to the viewports array. To capture multiple URLs, read a list from configuration or loop over an array of URLs and include a safe page identifier in each filename. Avoid placing arbitrary URL text directly in filenames; normalize it to a short slug and ensure duplicate URLs do not overwrite each other’s images.
For each page and viewport, use a fresh browser page as in the example. This isolates cookies and page state between captures. If the target requires authentication, provide credentials through GitHub Actions secrets and have the script sign in or set the required cookies. Never commit passwords or session cookies into the workflow file or repository.
For device emulation, import a Playwright device descriptor and use its settings when creating the page or browser context. Descriptors include device-related configuration beyond viewport dimensions, so keep the chosen descriptor consistent across runs. Consult the current device emulation reference for supported descriptors and usage.
5. Archive captures or detect visual changes
An archive answers “What did the page look like on this date?” A visual regression check answers “Did this capture differ from an accepted baseline?” These are different workflows. For regression testing, keep the browser, operating system, viewport, device scale factor, fonts, and relevant page state stable. Playwright notes that browser rendering can vary with the host operating system, browser version, settings, hardware, power source, and headless mode. See Playwright’s visual comparison guide.
Dynamic content such as timestamps, rotating promotions, personalized recommendations, and animated areas can create image differences even when the layout is healthy. Stabilize test data where possible, wait for the relevant page state, and use Playwright’s documented screenshot comparison controls for intentionally variable regions. Review new baselines rather than automatically accepting every change.
Workflow artifacts work for short-term review. For longer history, download images into a storage system with an explicit retention policy. Decide who can access the images, how long to keep them, and whether old captures should be deleted automatically. Do not assume an uploaded artifact is a permanent archive.
6. cURL, Python, and Node.js with ScreenshotNeo
If you want scheduled captures without installing or maintaining a browser in the workflow, ScreenshotNeo is a website screenshot API and MCP server. Your scheduler can call its API once per viewport, passing the desired width and height. Check the ScreenshotNeo API documentation for current parameter names and supported options.
For example, this cURL call requests a screenshot at a 1440 by 900 viewport. Schedule the command using GitHub Actions or another scheduler, and change the viewport values for additional captures.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-d width=1440 \
-d height=900 \
-o shot-desktop.webp
Python equivalent:
import requests
params = {
"access_key": "YOUR_API_KEY",
"url": "https://stripe.com",
"width": 1440,
"height": 900,
}
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params=params,
timeout=90,
)
r.raise_for_status()
with open("shot-desktop.webp", "wb") as f:
f.write(r.content)
Node.js equivalent:
const q = new URLSearchParams({
access_key: process.env.SCREENSHOTNEO_API_KEY,
url: 'https://stripe.com',
width: '1440',
height: '900',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot-desktop.webp', image));
Repeat the request with the tablet and mobile dimensions and distinct output filenames. Keep the API key in a secret store or environment variable. The API supports many capture options, including full-page captures, device presets, custom CSS and JavaScript, element selection, waiting conditions, and caching; consult the docs for exact parameter names and behavior.
Or skip the browser setup
ScreenshotNeo lets a scheduled job request an image with one API call per viewport. Cookie banners, newsletter popups, and chat widgets are removed before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers say which page verdict occurred and whether it was billed. An MCP server gives AI agents tools to take screenshots, inspect page information, and capture PDFs.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo includes 1,000 screenshots a month free with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan. Sign up for the free plan.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
npm ci fails |
The lockfile is missing or out of sync with package.json. |
Run npm install locally, commit both files, and retry. |
| Playwright reports that Chromium is missing | The browser binary was not installed in the runner environment. | Keep the browser install step in the workflow and install the same browser that the script launches. |
| Navigation times out | The site is slow, blocked, or waiting on resources that do not settle. | Use a suitable navigation condition and timeout; wait for a page-specific selector after DOM content loads. Check whether the runner can reach the site. |
| The screenshot is blank or missing content | The page may render content after navigation, require authentication, or need a page-specific wait. | Wait for a visible content selector and confirm the page state in a browser. Supply required authentication securely. |
| Images or fonts appear late | The page’s content and fonts may load after the selected wait condition. | Wait for a meaningful element or explicit page-ready signal. Avoid assuming that network idle means all page content is visually ready. |
| Captures differ between runs | Dynamic content or a changed browser/runtime can alter rendering. | Stabilize data and environment, fix viewport and scale factor, and handle known dynamic regions as described in Playwright’s visual comparison guidance. |
| No artifact appears | The script failed before writing files, or the upload path does not match the output directory. | Inspect the capture step logs, confirm OUTPUT_DIR, and keep the upload path aligned with it. |
| Scheduled run did not start at the expected minute | Scheduled workflow start times can be delayed by GitHub Actions load. | Allow for delay, use manual dispatch when necessary, and monitor workflow completion if captures are operationally important. |
Performance, reliability, and cost
- Runtime: Each viewport requires page navigation and image rendering. Reusing a browser process, as the script does, avoids launching Chromium for every size. Keep each viewport in its own page when isolation is useful.
- Image size: Full-page images and high device scale factors increase output dimensions and storage. Use viewport captures for breakpoint checks and choose image format and scale to suit the review task.
- Reliability: Use explicit timeouts, a meaningful readiness condition, a predictable output path, and artifact upload even after capture errors so partial results and logs remain available. Consider retry behavior carefully; retries can create duplicate images and additional API calls.
- Repeatability: Pin runtime and dependencies where practical. A stable runner alone does not freeze remote page content, third-party scripts, fonts, or personalization.
- Cost: A self-hosted script uses your compute and storage resources; GitHub Actions usage and retention depend on your account and configuration. A hosted API removes browser installation and maintenance from your workflow but has plan limits and request costs. ScreenshotNeo’s free tier includes 1,000 screenshots per month; paid plans begin at $5 for 3,000, with higher plans available.
FAQ
Should I use a screenshot archive or visual regression testing?
Use an archive when people need dated images for inspection. Use visual regression testing when you need to compare captures against an approved baseline and review meaningful differences.
Do viewport dimensions fully simulate a phone?
No. Width and height test responsive layout at those CSS dimensions. Device emulation can also apply properties such as user agent and touch behavior.
Can the schedule run more than once a day?
Yes. Change the cron schedule to the recurrence you need, using GitHub Actions’ documented syntax and accounting for possible start delays.
How should I choose image retention?
Base it on how long reviewers need to inspect history and the storage limits or policies of your artifact destination. Set retention explicitly and periodically verify it matches the team’s review process.


