Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Puppeteer does not create URL-based screenshot filenames for you. After navigation, read the final address with page.url(), parse it with the standard WHATWG URL class, convert the host and relevant URL components into a filesystem-safe slug, append a short hash to prevent collisions, and pass the resulting path to page.screenshot({path}). Reading the URL after navigation matters because redirects can change the page you actually captured.

The complete Puppeteer implementation

This example creates readable names such as example.com__docs-start__lang=en__full__a1b2c3d4e5.png. It sanitizes unsafe characters, limits the human-readable portion, distinguishes viewport and full-page captures, and adds a deterministic SHA-256 suffix.

import puppeteer from 'puppeteer';
import crypto from 'node:crypto';
import path from 'node:path';
import fs from 'node:fs/promises';

function safePart(value) {
  return value
    .normalize('NFKC')
    .replace(/[<>:"/\|?*u0000-u001F]/g, '-')
    .replace(/s+/g, '-')
    .replace(/-+/g, '-')
    .replace(/^[-.]+|[-.]+$/g, '')
    .slice(0, 140) || 'index';
}

function screenshotName(rawUrl, {fullPage = false} = {}) {
  const u = new URL(rawUrl);
  const host = safePart(u.hostname);
  const pathname = safePart(
    decodeURIComponent(u.pathname)
      .replace(/^/+|/+$/g, '')
      .replaceAll('/', '-')
  );
  const query = u.search ? safePart(u.search.slice(1)) : '';
  const identity = [host, pathname, query].filter(Boolean).join('__');
  const mode = fullPage ? '__full' : '__viewport';
  const digest = crypto.createHash('sha256').update(u.href).digest('hex').slice(0, 10);
  return `${identity || 'page'}${mode}__${digest}.png`;
}

await fs.mkdir('screenshots', {recursive: true});
const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  const target = 'https://example.com/docs/start?lang=en';
  await page.goto(target, {waitUntil: 'networkidle2'});
  const finalUrl = page.url();
  const fullPage = true;
  const filename = screenshotName(finalUrl, {fullPage});
  await page.screenshot({
    path: path.join('screenshots', filename),
    fullPage
  });
  console.log({requested: target, captured: finalUrl, filename});
} finally {
  await browser.close();
}

The URL properties used here have separate meanings: hostname is the host without the port, pathname excludes query and fragment, and search contains the query string. Puppeteer’s Page.url() returns the current page URL, while ScreenshotOptions.path chooses the output location and infers the image type from its extension. A .png extension is a safe lossless default; use .jpeg only when you also set JPEG quality.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The fullPage option captures the full document; without it, Puppeteer captures the current viewport (unless you provide a clip). Include the mode in the name when both variants can exist.

How the filename is assembled

Host and path are the default identity

For https://example.com/docs/start, the readable portion becomes example.com__docs-start. Host plus path is usually enough for people browsing a screenshot directory.

Queries are conditional

Query parameters belong in the name only when they change the rendered page. A language, product ID, or filter generally matters; an analytics parameter usually does not. If your inputs can present parameters in different orders, canonicalize them before naming so equivalent URLs do not create needless variants. For example:

function sortedSearchParams(u) {
  const pairs = [...u.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b));
  return new URLSearchParams(pairs).toString();
}

You can replace u.search.slice(1) with sortedSearchParams(u) when parameter order is irrelevant to your application. Do not sort parameters blindly if the server treats order as significant.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fragments require an application decision

A fragment (the part after #) is client-side state and is often absent from the HTTP request. Server-rendered pages commonly produce the same document for different fragments, while a single-page application may render a completely different view. The sample hashes u.href, so fragment changes still produce different digests even though the visible slug omits the fragment. Add a sanitized fragment to the readable portion if operators need to see that state directly.

Sanitization protects every filesystem

The sanitizer replaces slashes, backslashes, colons, question marks, asterisks, quotes, angle brackets, control characters, and repeated separators. It also removes leading or trailing dots and hyphens, which avoids awkward names and Windows reserved trailing characters. Decode percent-encoded paths only when you control the input, and sanitize after decoding. Never allow a URL-derived value to become an arbitrary directory path.

Length limits and hashes prevent collisions

Long queries can exceed filesystem limits, and normalization or truncation can make two URLs look identical. The ten-character SHA-256 suffix keeps names deterministic while separating those cases. Keep the complete original URL in a manifest or JSON sidecar so the image remains auditable.

Capture the URL that was actually rendered

Call page.url() after the navigation and any interaction that changes the route. Reusing the requested string can mislabel a redirect, login flow, locale redirect, or client-side route. If a click changes the view, wait for that action to settle, then generate the filename from the new page.url().

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.goto(inputUrl, {waitUntil: 'networkidle2'});
await page.click('[data-next]');
await page.waitForNetworkIdle();
const renderedUrl = page.url();
const name = screenshotName(renderedUrl);
await page.screenshot({path: path.join('screenshots', name)});

For pages that never become network-idle because of polling or analytics, use a meaningful selector or a bounded delay instead of waiting forever. Create the destination directory first and use path.join(), not string-concatenated separators.

Policies for repeat captures and large crawls

Deterministic versus chronological names

  • Reproducible archive: use the canonical URL plus digest, overwriting or versioning deliberately.
  • Every run retained: append a sequence number or ISO timestamp. A timestamp intentionally makes each capture a new file.
  • Multiple visual variants: include viewport dimensions, device name, color scheme, or capture mode in the identity.

Keep a manifest

Write a JSON record containing the original input URL, final page.url(), filename, capture time, viewport, and options. This preserves traceability when a slug is truncated or a query is omitted.

Protect the output boundary

When URLs are untrusted, resolve the final path and verify it remains inside your intended screenshot directory before writing. Reject unexpected schemes and do not pass user-controlled strings as additional path segments.

Common failures and fixes

Symptom Likely cause Fix
TypeError: Invalid URL Input is relative or malformed. Require an absolute URL such as https://site.test/page, or resolve a relative value against a trusted base with new URL(value, base).
Different pages overwrite one another Query, fragment, device state, or mode was omitted. Include the rendering-changing component and retain the URL hash; add mode or viewport metadata.
Names contain encoded noise Percent-encoded path was used literally. Decode controlled input, then sanitize; leave undecodable or untrusted values encoded.
Windows write error Reserved characters, trailing dots/spaces, or excessive length. Use the sanitizer, trim the readable part, and append the digest.
Screenshot shows the wrong route Name was generated before redirect or client navigation completed. Wait for navigation/interaction, then call page.url().
Image type is unexpected Extension and screenshot options disagree. Choose .png, .jpeg, or .webp deliberately; Puppeteer infers type from the extension.
Capture hangs Continuous network activity prevents idle. Wait for a page-specific selector or bounded delay and set an overall timeout.
Files appear outside the target folder Path traversal through URL-derived text. Keep the generated value a single filename, use path.join(), and validate the resolved path.

Performance, reliability, and cost considerations

  • Launching one browser per URL is expensive. Reuse a browser and create isolated pages or contexts, while limiting concurrency to what your CPU and memory can sustain.
  • networkidle2 can improve completeness but costs time on asset-heavy sites. A selector that proves the content is ready is often faster and more reliable.
  • Full-page screenshots require more layout and image memory than viewport captures. Lazy-loaded images may need scrolling or an application-specific wait before capture.
  • Hashing the final URL is cheap compared with navigation. The expensive operations are browser startup, page loading, fonts, scripts, and image decoding.
  • For recurring jobs, deterministic names make retries idempotent; timestamps make auditing easier but can retain accidental duplicates.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF, so you can keep URL-derived naming in your own script while delegating browser capture. Its clean-shot steps accept cookie and consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the API details in the ScreenshotNeo documentation. cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js:

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(`${res.status} ${await res.text()}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));

ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Features include full-page and element capture, device presets, custom CSS/JavaScript, waits, request blocking, headers/cookies, geolocation, resizing, caching with a chosen TTL, signed links, asynchronous webhooks, bulk capture of 100 URLs per call, and a usage API. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Official API behavior to rely on

  • Puppeteer’s screenshot guide and Page API demonstrate page.screenshot({path}) after navigation: screenshots guide.
  • Page.url() returns the current page URL: Page.url API.
  • ScreenshotOptions.path controls the destination and extension-based image type: ScreenshotOptions.
  • fullPage controls full-document capture: ScreenshotOptions.
  • Node’s WHATWG URL component behavior is documented in the Node.js URL API.

Frequently Asked Questions

Should the filename include the URL scheme?

Usually no. The hostname and path are readable enough, while the scheme can be retained in the manifest and covered by the digest.

How do I name screenshots when two captures use different viewport widths?

Add a normalized viewport token such as __1280x720 to the filename, or store viewport metadata in the manifest if names must remain short.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I use the URL pathname directly as a filename?

No. It can contain separators, encoded characters, reserved names, or traversal-like input. Convert it to one sanitized filename component first.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.