You take a fullPage screenshot and the top of the page looks right, but further down there are grey boxes where the photos should be, sections that are blank, and a cookie banner sitting in the middle of the image. We built a test page with the usual culprits and measured what Puppeteer 24, Playwright and our API actually capture.
The test page: a fixed 60 px header, a 100vh hero, 10 images (4 lazy-loaded by an IntersectionObserver script, 4 with native loading="lazy", 2 in scroll-reveal sections that fade in) and a fixed cookie bar at the bottom. Viewport 1280×800, page.screenshot({ fullPage: true }) after waitUntil: 'load':
| Setup | Images captured |
|---|---|
Puppeteer, plain fullPage: truePlaywright fullPage: true: identical | 0 of 10 |
| Same, page scrolled to the bottom and back first | 8 of 10 reveal sections caught mid-fade |
| Scroll + wait for images + transitions off (the fix below) | 10 of 10 |
| Viewport resized to the page height (a common workaround) | Image 12,900 px tall the 100vh hero grew to 6,850 px |
Even native loading="lazy" images were missing: Chrome only loads them near the viewport, and a full-page capture enlarges the capture area without scrolling. Nothing errors; you get a PNG of the right height with empty boxes. In every run the fixed cookie bar was painted once, at 750 to 800 px, i.e. over the first screen and not at the bottom of the image.
Scroll one step at a time so lazy loaders fire, scroll back up, wait until every <img> has finished, switch transitions off so reveal sections show their final state, and hide fixed bars. Keep the viewport at its normal height: resizing it to the full page height is what blows up 100vh sections. Save as full-page.mjs, then npm i puppeteer and node full-page.mjs https://your-page out.png:
// full-page.mjs: full-page screenshot that includes lazy-loaded images.
// Usage: node full-page.mjs https://example.com out.png
import puppeteer from 'puppeteer';
const [url = 'https://example.com', out = 'full-page.png'] = process.argv.slice(2);
const browser = await puppeteer.launch({ args: ['--no-sandbox'] }); // --no-sandbox: Ubuntu 23.10+ / Docker
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 }); // keep the real height: 100vh sections size from it
await page.goto(url, { waitUntil: 'networkidle2' });
// 1. Scroll through the page so IntersectionObserver and loading="lazy" images start loading.
await page.evaluate(async () => {
const step = innerHeight * 0.8;
for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
scrollTo(0, y);
await new Promise(r => setTimeout(r, 150));
}
scrollTo(0, 0);
});
// 2. Wait until every <img> has finished (loaded or failed), max 5 s.
await page.waitForFunction(
() => [...document.images].every(img => img.complete),
{ timeout: 5000 }
).catch(() => console.warn('some images were still loading'));
// 3. Finish scroll-reveal transitions now, and hide fixed bars (cookie banners, chat widgets):
// they would be painted once, over the first screen.
await page.addStyleTag({ content: `
*, *::before, *::after { transition: none !important; animation-duration: 0s !important; animation-delay: 0s !important; }
.cookie, #onetrust-banner-sdk, .intercom-lightweight-app { display: none !important; }` });
await page.screenshot({ path: out, fullPage: true });
await browser.close();
console.log('saved', out);
On the test page this captured 10 of 10 images, the hero at its real 800 px and no cookie bar. Notes:
page.evaluate and waitForFunction blocks work unchanged; use chromium.launch(), page.setViewportSize() and waitUntil: 'networkidle'.y < Math.min(document.documentElement.scrollHeight, 30000), or it never ends.data-bg load during the scroll but aren't in document.images; add a short fixed delay if you rely on them.full_page: true on /v1/screenshot already scrolls the page (up to 16,000 px or 3.5 seconds), waits for whatever the scroll started loading, fast-forwards CSS animations and transitions, and keeps the viewport height, so the test page came back with 10 of 10 images and an 800 px hero. Fixed bars are still painted over the first screen, as in a browser; list them in hide_selectors (with them hidden, the cookie bar was gone and nothing else changed). Node 18+ (save as full-page-api.mjs, set SHOTMINT_KEY):
import { writeFileSync } from 'node:fs';
const res = await fetch('https://shotmint.creasework.com/v1/screenshot', {
method: 'POST',
headers: { 'X-API-Key': process.env.SHOTMINT_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
url: process.argv[2] ?? 'https://en.wikipedia.org/wiki/Lazy_loading',
full_page: true,
hide_selectors: '#onetrust-banner-sdk, .cookie-banner', // fixed bars you don't want in the image
}),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
writeFileSync('full-page.png', Buffer.from(await res.arrayBuffer()));
console.log('full-page.png written, credits left:', res.headers.get('x-credits-remaining'));
Python with requests:
import os, sys, requests
url = sys.argv[1] if len(sys.argv) > 1 else "https://en.wikipedia.org/wiki/Lazy_loading"
res = requests.post(
"https://shotmint.creasework.com/v1/screenshot",
headers={"X-API-Key": os.environ["SHOTMINT_KEY"]},
json={"url": url, "full_page": True, "hide_selectors": "#onetrust-banner-sdk, .cookie-banner"},
)
res.raise_for_status()
open("full-page.png", "wb").write(res.content)
print("full-page.png written, credits left:", res.headers.get("x-credits-remaining"))
Both scripts were run against the live API (1 credit each) and returned a 1280×5081 PNG of the Wikipedia article. The 0/8/10-of-10 numbers above come from the same test page rendered by Puppeteer 24, Playwright and our renderer on 20 October 2026. You can also try a capture without a key in the full-page screenshot tool.
img.complete on every image.hide_selectors in the API).No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire). From a script, an AI agent, Make or Zapier, POST /v1/keys/free (no body) returns a key as JSON (openapi.json, no-code recipes).
All guides: guides · integrations · comparisons