How to Take Scheduled Screenshots of Competitor Landing Pages
Capture competitor landing pages on a schedule with Playwright, compare changes reliably, and choose between a self-managed workflow and a hosted API.
To take scheduled screenshots of competitor landing pages, use a browser automation script to render and capture each page, then run that script on a recurring schedule with cron or a hosted workflow. Keep the browser, viewport, and capture mode consistent so visual differences reflect the page rather than your capture setup. If you also need change alerts, add image comparison and a delivery channel; saving screenshots alone creates an archive, not a monitoring feed.
This guide uses Playwright with Node.js, then shows ways to schedule it, compare captures, and handle failures. The same separation applies to other browser automation tools: the browser captures; a scheduler decides when.
1. Choose what the workflow should do
Start by deciding whether you need an image history or notifications about changes. Those are related but distinct outcomes.
| Need | Workflow |
|---|---|
| Visual history | Capture on a schedule and retain each image with its URL and timestamp. |
| Change alerts | Capture, compare the new image with a previous one, then send a notification when the difference passes a threshold. |
| Auditable monitoring | Keep captures and comparison results, record failed runs, and alert on repeated failures as well as page changes. |
A managed scheduled-capture service may bundle the recurring trigger, storage, and email or webhook delivery. Check its current capture controls, quota, retention, export, and notification terms before relying on it. For example, [Allscreenshots documents scheduled captures, retained run history, and email or webhook delivery](https://docs.allscreenshots.com/guides/scheduled-screenshots); these are vendor-described capabilities, and plan terms can change.
2. Build a Playwright screenshot script
The following runnable Node.js example reads URLs from a JSON file, captures a consistent full-page image for each one, and names the output with a timestamp. It deliberately treats a failed page as a failed run instead of silently archiving an error page as a valid capture.
Install Playwright
mkdir competitor-captures
cd competitor-captures
npm init -y
npm install playwright
npx playwright install chromium
Create urls.json:
[
"https://example.com/pricing",
"https://example.org/product"
]
Create capture.mjs:
import { chromium } from 'playwright';
import { mkdir, readFile } from 'node:fs/promises';
const urls = JSON.parse(await readFile(new URL('./urls.json', import.meta.url), 'utf8'));
const outputDir = new URL('./captures/', import.meta.url);
await mkdir(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
let failed = false;
try {
for (const rawUrl of urls) {
const url = new URL(rawUrl);
if (!['http:', 'https:'].includes(url.protocol)) {
console.error(`Skipping unsupported URL scheme: ${rawUrl}`);
failed = true;
continue;
}
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1
});
try {
const response = await page.goto(url.href, {
waitUntil: 'domcontentloaded',
timeout: 45000
});
if (!response || response.status() >= 400) {
throw new Error(`Navigation returned ${response?.status() ?? 'no response'}`);
}
// Let client-side layout settle without waiting for every analytics request.
await page.waitForTimeout(1500);
const stamp = new Date().toISOString().replaceAll(':', '-');
const safeHost = url.hostname.replaceAll(/[^a-zA-Z0-9.-]/g, '_');
const path = url.pathname.replaceAll(/[^a-zA-Z0-9.-]/g, '_').slice(0, 80) || 'home';
await page.screenshot({
path: new URL(`${safeHost}${path}-${stamp}.png`, outputDir).pathname,
fullPage: true,
animations: 'disabled'
});
console.log(`Captured ${url.href}`);
} catch (error) {
failed = true;
console.error(`Failed ${url.href}: ${error.message}`);
} finally {
await page.close();
}
}
} finally {
await browser.close();
}
if (failed) process.exitCode = 1;
Run it locally with node capture.mjs. Playwright supports viewport, element, and full-page screenshots, and PNG, JPEG, and WebP output in its screenshot tooling. Choose the format and capture area deliberately and keep them fixed across runs. [Playwright screenshot documentation](https://playwright.dev/mcp/tools/screenshots) describes these capture modes.
Useful capture choices
| Choice | Use it when | Tradeoff |
|---|---|---|
| Viewport | You care about the first screen or a fixed campaign layout. | Below-the-fold content is omitted. |
| Full page | You need the whole landing page, including content below the fold. | Very long pages produce larger images and may trigger lazy-loading or layout quirks. |
| Element selector | You only need a stable pricing table, hero, or product section. | Selector changes can break capture; the result omits surrounding context. |
| Retina scale | You want more pixel detail for inspection. | Files are larger, and scale must remain consistent for comparisons. |
| JPEG or WebP | Storage or transfer size matters more than lossless pixel comparison. | Compression can introduce visual differences; use the same format and settings for every run. |
For dynamic pages, prefer waiting for a meaningful selector, such as the pricing grid, instead of an arbitrary long delay. A fixed short delay after that can allow fonts and transitions to settle. Network-idle waiting can hang or time out on pages that keep analytics or streaming requests open. Disable animations if your tool supports it, and hide volatile elements such as rotating testimonials or visitor counts only when excluding them fits the monitoring goal.
3. Schedule recurring runs
Use cron on a persistent machine
On a Linux host with Node.js, install the browser dependencies once and add a crontab entry. This example runs daily at 08:15 UTC and appends logs to a file:
15 8 * * * cd /path/to/competitor-captures && /usr/bin/node capture.mjs >> capture.log 2>&1
Use the absolute path to the Node executable and project directory. Cron has a smaller environment than an interactive shell, so do not assume it inherits your PATH, shell profile, or local timezone. Verify the host timezone and use UTC where a stable global schedule matters. Create the archive directory on persistent storage; ephemeral build agents may discard files when the job ends.
Use a hosted workflow
A hosted CI workflow can run the script on a schedule without maintaining a server. Configure the schedule in the workflow’s cron syntax, install Node and Playwright’s Chromium browser in the job, run node capture.mjs, and upload the captures/ directory to persistent artifact or object storage. Set a timeout, avoid overlapping runs, and decide how failed captures should affect the workflow status.
A community-maintained [GitHub screenshot action](https://github.com/guibranco/github-screenshot-action) documents a JSON URL list and scheduled workflow approach with capture settings and retries. It is a community project, not an official GitHub feature or endorsement; check its current version and maintenance before adopting it.
Choose a sensible cadence
- Daily or weekly: useful for ordinary landing-page and positioning history.
- Hourly or more often: reserve for short-lived launches or pricing changes where frequent checks are useful.
- Custom cadence: align the schedule with the business question and the cost of storing and reviewing captures.
More frequent captures create more files and more opportunities to see transient experiments, ads, or rotating content. If the site changes through gradual experiments, keep timestamps and optionally capture more than once before concluding a lasting change occurred.
4. Compare captures and alert on meaningful changes
A screenshot archive answers “what did the page look like then?” To answer “did it change?” compute a difference between the newest image and a prior capture. A raw pixel comparison is sensitive to fonts, anti-aliasing, animation, ads, and small layout shifts. Normalize the capture setup first, then tune the comparison threshold against real examples. Do not automatically treat every changed pixel as a business-significant change.
For a test-style baseline, Playwright Test can compare screenshots with toHaveScreenshot(). Its visual comparison uses reference images and supports options such as maxDiffPixels and a stylesheet to hide volatile content. The docs say the assertion takes captures until two consecutive screenshots match before comparing the final capture with the expected image. This is a test-runner behavior; a simple archive script does not get that comparison automatically. See [Playwright visual comparisons](https://playwright.dev/docs/test-snapshots).
For ongoing competitor monitoring, establish these rules:
- Compare images with the same URL, viewport, browser, device scale, capture mode, and format.
- Ignore known volatile regions only when they are irrelevant to the question you are monitoring.
- Set a pixel-difference threshold and inspect initial alerts to understand false positives.
- Send a link to the before and after images, the capture time, and a small difference summary.
- Report capture failures separately from “no change”; an unavailable page is not evidence that the page stayed the same.
5. Make runs reliable and maintainable
- Use bounded waits: navigation and selector waits should have timeouts so one broken target cannot occupy the runner indefinitely.
- Retry selectively: retry transient navigation errors with a limit and a pause; avoid retrying permanent 4xx responses repeatedly.
- Prevent overlap: a run should not begin while a previous run is still writing the same outputs.
- Keep an index: record URL, requested run time, actual capture time, status, viewport, browser version, image path, and any error.
- Retain deliberately: define a retention window or move captures to durable object storage; avoid unbounded local disk growth.
- Protect secrets: if a target requires authentication, use a secret store and do not commit cookies, authorization headers, or session state to the URL list.
- Respect access controls: capture public pages you are permitted to access, and do not attempt to bypass CAPTCHAs or access restrictions.
Browser output can vary with the host OS, browser version, settings, hardware, power source, and headless mode. Keep the capture environment stable when visual comparison matters. This caveat is documented in [Playwright’s visual comparison guidance](https://playwright.dev/docs/test-snapshots).
6. Costs and performance
Self-managed captures use your compute, storage, and notification resources. Full-page images, higher device scale, frequent schedules, and many URLs increase runtime and archive size. A browser launch per URL is simple but slower than reusing a browser; a shared browser with isolated pages can improve throughput, while bounded concurrency prevents exhausting memory or overloading target sites.
Estimate volume before choosing a cadence: URLs × captures per day × retention days gives the approximate number of stored images. Multiply by average image size for a rough storage estimate. The dossier contains no verified comparative price or performance figures for browser hosting or third-party services, so check current provider terms rather than assuming a particular cost.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser executable missing | Playwright package is installed but its browser was not installed in this environment. | Run npx playwright install chromium during setup, including in the scheduled job image. |
| Navigation timeout | Slow target, blocked automation, or a page whose network never becomes idle. | Use a bounded navigation timeout and a narrower readiness condition such as domcontentloaded plus a target selector. Diagnose access restrictions rather than trying to evade them. |
| Screenshot is blank or incomplete | Capture began before client rendering, fonts, or lazy content finished. | Wait for a visible page landmark; for full-page captures, scroll or otherwise trigger lazy loading if required by the page and your capture method. |
| Every run looks different | Viewport, browser, host, animation, rotating content, or device scale changed. | Pin the environment and capture settings; disable animation and mask only irrelevant volatile elements. |
| Too many change alerts | Pixel diffs include ads, counters, rotating banners, antialiasing, or subtle layout changes. | Hide known volatile areas, stabilize the environment, and tune the threshold using reviewed examples. |
| No files remain after a scheduled job | The runner uses temporary storage or artifacts were not uploaded. | Write to a persistent volume or explicitly upload the capture directory to durable storage. |
| Runs overlap or output gets mixed | A capture takes longer than the schedule interval or filenames collide. | Use unique timestamps, a concurrency lock, and a schedule interval appropriate to the longest run. |
| Alerts say unchanged after a failed capture | The workflow treats missing output as a successful comparison. | Represent capture status separately and suppress change conclusions when either comparison image is missing or invalid. |
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. A scheduled worker or workflow can call its one-request capture endpoint and save the returned image. See the API documentation for options, and put the API key in your scheduler’s secret storage.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Replace the example URL with a competitor landing page. The caller still controls scheduling, archive retention, comparison, and alert delivery. ScreenshotNeo’s clean-capture options can accept consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000. Create a free account and start with 1,000 screenshots a month, no card required.
Frequently asked questions
Should I capture the full page or just the first screen?
Use full-page capture when changes anywhere on the landing page matter. Use a viewport or selected element when the question is specifically about the hero, offer, or pricing block and you want a smaller, steadier comparison.
Can screenshots alone tell me what changed semantically?
No. A screenshot records appearance. To identify a text or structural change, add a separate text or DOM extraction step, or review the before-and-after images.
How do I avoid treating a temporary experiment as a permanent change?
Keep the capture timestamps and require a change to persist across more than one scheduled run before escalating it. For costly alerts, add a review step.
Does scheduling automatically provide change alerts?
No. A self-managed script needs comparison and notification logic. A hosted service may bundle them, but confirm its current plan, retention, and delivery terms.


