ScreenshotNeo

BlogHow-to

How to Schedule Screenshots of a Website After Scrolling to a Specific Section

Use Playwright to capture a section or the viewport around it, then schedule the script with GitHub Actions. Includes runnable code and scheduling caveats.

By the ScreenshotNeo team4 October 202610 min read

Use a browser automation script to open the page, wait for the section, and capture either the section itself or the viewport after scrolling to it. Then run that script on a schedule. This guide uses Playwright for capture and GitHub Actions as one scheduler; the two jobs are separate, so you can replace the scheduler without changing the browser logic.

Choose the framing first: a locator screenshot crops to the section element, a viewport screenshot shows the section with nearby content, and a full-page screenshot captures the whole page. Playwright locator screenshots attempt to scroll the target into view after actionability checks. Playwright screenshots documentation and the Locator API describe these capture behaviors.

1. Choose what the screenshot should show

Capture mode What appears Use it when
Element screenshot The matched section, cropped to its bounds You need a focused image of one component or section.
Viewport after scrolling The visible browser viewport, with the section and surrounding page context You want readers or reviewers to see where the section sits on the page.
Full page The full scrollable page You need a record of the whole page, not a particular viewport position.

For a scrollable container, an element screenshot includes only the container content currently scrolled into view. If the target is inside a nested scrolling panel, scroll that panel to the desired position before capturing. A full-page screenshot is not equivalent to capturing the viewport after scrolling to a section.

2. Create a Playwright capture script

This example is an ES module for Node.js. It takes the page URL, a CSS selector for the section, and a capture mode from environment variables. It writes the image to a local file. The script uses a fixed viewport and waits for the target locator rather than relying on a short, arbitrary sleep.

import { chromium } from 'playwright';

const url = process.env.PAGE_URL;
const selector = process.env.SECTION_SELECTOR;
const mode = process.env.CAPTURE_MODE ?? 'viewport'; // element | viewport | full
const output = process.env.OUTPUT ?? 'section.png';

if (!url || !selector) {
  throw new Error('Set PAGE_URL and SECTION_SELECTOR.');
}
if (!['element', 'viewport', 'full'].includes(mode)) {
  throw new Error('CAPTURE_MODE must be element, viewport, or full.');
}

const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage({
    viewport: { width: 1440, height: 1000 },
    deviceScaleFactor: 1,
  });
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });

  const section = page.locator(selector).first();
  await section.waitFor({ state: 'visible', timeout: 30_000 });

  // If the section is populated asynchronously, wait for a meaningful state
  // before capture, for example a child selector or expected text.
  if (mode === 'element') {
    await section.screenshot({ path: output, animations: 'disabled' });
  } else {
    await section.scrollIntoViewIfNeeded();
    // A sticky header can cover the target after scrolling. Apply a site-appropriate
    // scroll margin in CSS or adjust scroll position if that affects your page.
    if (mode === 'full') {
      await page.screenshot({ path: output, fullPage: true, animations: 'disabled' });
    } else {
      await page.screenshot({ path: output, animations: 'disabled' });
    }
  }
} finally {
  await browser.close();
}

Install the runtime and browser in a project directory:

npm init -y
npm install --save-dev playwright
npx playwright install chromium

Save the script as capture.mjs. A local run might look like this:

PAGE_URL='https://example.com/pricing' \
SECTION_SELECTOR='#plans' \
CAPTURE_MODE=viewport \
OUTPUT=pricing.png \
node capture.mjs

Replace the example URL and selector with the page you control or are authorized to capture. For a more stable target, use a maintained ID or a locator based on the section’s accessible role and name. Avoid selectors tied to generated class names that change with each build.

3. Adjust the script for real pages

Wait for the section to be ready

domcontentloaded means the initial HTML document has been parsed; it does not guarantee that client-rendered content, images, or API data are ready. Wait for the state your capture depends on. For example:

await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
const section = page.locator('#plans');
await section.waitFor({ state: 'visible', timeout: 30_000 });
await section.getByRole('heading', { name: 'Plans' }).waitFor({ state: 'visible' });

If content appears after an API call, wait for a visible result or a specific response that your own page exposes. Use a fixed delay only when the page provides no observable readiness condition; delays add runtime and can still be too short or unnecessarily long.

Use a semantic locator when practical

A CSS selector is convenient for a script that takes a selector as configuration. On a page with accessible markup, a role/name locator can be easier to understand and maintain:

const section = page.getByRole('region', { name: 'Pricing plans' });
await section.waitFor({ state: 'visible' });

The page must expose a matching accessible role and name for that locator to work. If it does not, add an accessible label where you control the page or use a stable selector.

Handle sticky headers and target position

scrollIntoViewIfNeeded() brings the element into view, but a fixed or sticky header may overlap it. If you control the site, give the target a suitable scroll-margin-top. Otherwise, scroll by a measured offset and verify the final position for that page. Do not assume the same offset works across pages or viewport sizes.

Capture dynamic and visually unstable content

For animated elements, Playwright screenshot options can disable animations. If a timestamp, rotating banner, or live value makes every image differ, consider masking that region or hiding it with a dedicated capture stylesheet when your use case permits. Avoid hiding content whose state is the reason for taking the screenshot.

Set image format and scale deliberately

Playwright supports screenshot output options such as path, type, quality for JPEG, full-page capture, and device scale factor through the browser context. PNG is suitable for lossless output; JPEG can reduce file size for photographic pages but is lossy. Keep viewport dimensions and device scale factor consistent across runs if you compare images over time. See the screenshot API guide for supported options.

4. Schedule it with GitHub Actions

Create .github/workflows/section-screenshot.yml in the repository. This example runs at 17 minutes past each hour, installs Playwright and Chromium, runs the capture, and uploads the image as a workflow artifact.

name: Scheduled section screenshot

on:
  schedule:
    - cron: '17 * * * *'
  workflow_dispatch:

jobs:
  capture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - name: Capture section
        run: node capture.mjs
        env:
          PAGE_URL: https://example.com/pricing
          SECTION_SELECTOR: '#plans'
          CAPTURE_MODE: viewport
          OUTPUT: section.png
      - uses: actions/upload-artifact@v4
        with:
          name: section-screenshot
          path: section.png
          if-no-files-found: error

Commit the workflow to the repository’s default branch; scheduled workflows run against the latest commit on that branch. The example’s artifact is a convenient run output. Choose an artifact retention period and delivery destination appropriate to your needs, and check the workflow logs when a run fails.

GitHub Actions schedule times use UTC by default. GitHub supports an IANA timezone setting for scheduled workflows and documents a minimum interval of once every five minutes. Scheduled runs can be delayed during periods of high load, especially near the start of an hour, and queued runs can be dropped if load is sufficiently high. Scheduling at an off-minute such as :17 may reduce contention but does not guarantee an exact start time. A public repository’s scheduled workflow can be disabled after 60 days without repository activity. Check GitHub’s current schedule event documentation when configuring a production workflow.

If you set a timezone in workflow YAML, use a valid IANA name supported by GitHub, such as America/New_York, and account for daylight-saving changes if your requirement is tied to local wall-clock time. If you leave it unset, express the cron schedule in UTC.

5. Pick a delivery and retention path

The workflow above stores the output as an artifact associated with its run. For a recurring archive, decide how long each image must remain available and where it should live. Common design questions include:

  • Should each run overwrite a current image, or should the timestamp be part of the filename?
  • Who needs access, and should the destination be private?
  • How long must captures be retained, and what is the storage cost?
  • Should a failed capture trigger an alert, and who owns the page selector when the site changes?

Keep credentials in repository or environment secrets if your target requires authentication. Do not place tokens, cookies, or passwords in the workflow file or in a public artifact. Restrict artifact access and retention to the actual monitoring need.

6. Reliability and performance considerations

  • Target stability: selectors are part of the monitoring configuration. Prefer stable IDs, accessible names, or selectors maintained with the page. A redesign can invalidate any selector.
  • Readiness: wait for the content being measured. Waiting for every network request to stop can hang on pages with analytics or persistent connections; a specific visible condition is often more targeted.
  • Repeatability: fix the viewport, browser version where practical, scale factor, and capture mode. Differences in fonts, device pixel ratio, or loaded content can create image changes that do not reflect the page change you care about.
  • Runtime: browser startup and page load dominate many captures. Keep the browser headless, avoid unnecessary page waits, and capture only the needed region when a full page is not required.
  • Retries: a retry can help with transient navigation failures, but repeated retries can conceal a broken selector or persistent site outage. Record failures and make the retry policy explicit.
  • Scheduler timing: GitHub scheduled runs are suitable for periodic snapshots where some start-time variation is acceptable. For a strict deadline, assess a scheduler against its documented timing, retry, alerting, timezone, execution, and retention behavior.

GitHub Actions usage and storage depend on your repository and plan. Estimate frequency, browser runtime, and artifact retention from your intended schedule, and consult the current GitHub billing and artifact documentation for applicable limits and charges. No capture-time or cost benchmark is implied here.

7. Troubleshooting

Symptom Likely cause Fix
Locator wait times out The selector is wrong, the section is not rendered, or the page is behind authentication. Inspect the page DOM and selector locally; wait for the actual rendered state; configure authorized authentication securely.
Screenshot shows a loading state The section became visible before its data finished loading. Wait for a meaningful child, text, or page-specific ready condition before capture.
Target is hidden under a sticky header Scrolling placed the element behind fixed page chrome. Use a suitable scroll margin or a page-specific offset, then verify the resulting viewport.
Element image contains only part of a panel The target is in a scrollable container whose other content is outside its current scroll position. Scroll the container to the desired position, or capture the page viewport/full page if that is the required framing.
Works locally, fails in Actions Browser dependencies, environment differences, missing secrets, or a different default branch state. Install Chromium with Playwright’s CI command, inspect logs, configure secrets, and confirm the workflow is on the default branch.
No scheduled run at the expected minute Schedule delay, wrong UTC/local-time assumption, workflow not on default branch, or inactive public repository. Check Actions run history and cron timezone; remember that GitHub does not guarantee the exact start instant.
Image changes on every run Animation, timestamps, rotating content, fonts, or live data vary. Disable animation, mask or hide irrelevant volatile regions, and make browser configuration consistent.
Artifact step says file not found The capture script failed or wrote to a different path. Match OUTPUT to the upload path and use if-no-files-found: error to expose missing output.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its API takes a URL and returns an image or PDF; it does not perform the custom Playwright scroll-and-schedule workflow above, so use the browser script when the capture must target a section after scrolling. For ordinary URL captures, one API call avoids managing a browser installation. See the ScreenshotNeo API documentation.

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify 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. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Use its API for URL-based captures, and keep the Playwright workflow when you need a custom scroll position or scheduled browser behavior.

Sign up free for 1,000 screenshots a month, with no card required.

FAQ

Can I schedule a screenshot every five minutes?

GitHub documents five minutes as the shortest supported scheduled interval. Its scheduler can still delay or drop runs under load, so this does not guarantee a capture exactly every five minutes.

Does a locator screenshot scroll the target into view?

Playwright’s locator screenshot performs actionability checks and attempts to scroll the matched element into view. It crops the result to the element’s bounds.

How do I capture the section with the content around it?

Scroll the locator into view, then screenshot the page rather than the locator. That captures the current viewport with surrounding context.

Can I schedule this in local time?

GitHub Actions uses UTC by default and supports an IANA timezone option. Verify the current workflow syntax and consider daylight-saving transitions for local-time schedules.

Will this work on a page that requires login?

It can if the automation has authorized access and the login state is supplied securely. Store credentials or session material as secrets and avoid publishing authenticated screenshots as public artifacts.