How to Capture Website Screenshots With an AI Agent and Name Files From Page URLs
Use Playwright to capture a page with an AI agent, then safely derive a readable filename from its URL. Includes viewport, element, and full-page examples.
The reliable way to name screenshots from page URLs is to handle capture and naming as separate steps: navigate the browser, read the final URL, parse and sanitize it in your code, and pass the resulting path to Playwright’s screenshot API. Playwright lets you choose a screenshot path; deriving a safe name, avoiding collisions, and deciding what URL details to include are application responsibilities.
This guide uses Playwright for the browser work and Node.js for URL-to-filename logic. The same pattern works when an AI agent chooses the target page: let the agent request or navigate to a URL, but keep filename policy explicit and deterministic in your application.
1. Choose the capture scope
Decide what the file should show before naming it. Playwright supports a screenshot of the visible viewport, a specific element, or the full scrollable page. The CLI and MCP interfaces expose corresponding screenshot controls. See Microsoft Playwright’s Page API, CLI screenshot documentation, and MCP screenshot documentation.
| Scope | Use it for | Trade-off |
|---|---|---|
| Viewport | A current screen state, such as a dashboard or above-the-fold page | Content outside the visible area is omitted |
| Element | A particular component, form, chart, or card | The target must exist and be locatable at capture time |
| Full page | A long page or documentation article | Can produce a very tall image; verify lazy-loaded content and layout |
For an AI-driven task, make the scope part of the instruction or structured request. For example, “capture the pricing table element” is less ambiguous than “screenshot the pricing page.”
2. Install and run the Playwright example
In an empty project directory, initialize Node and install Playwright. Install the browser binary as well:
npm init -y
npm install playwright
npx playwright install chromium
Save the following as capture.mjs. It accepts the target URL as an argument, navigates to it, waits for the page load event, constructs a filesystem-safe filename from the final URL, and saves a full-page PNG. It creates the output directory if needed and appends a short hash so different URLs that normalize to the same readable name are less likely to overwrite one another.
import { chromium } from 'playwright';
import { createHash } from 'node:crypto';
import { mkdir } from 'node:fs/promises';
import path from 'node:path';
function filenameFromUrl(input, extension = 'png') {
const url = new URL(input);
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('Only http and https URLs can be captured');
}
// Query strings and fragments may contain tracking or sensitive values.
// Omit them unless your project deliberately allowlists meaningful parameters.
const source = `${url.hostname}${url.pathname}`;
const readable = source
.normalize('NFKD')
.replace(/[\\u0300-\\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 100)
.replace(/-+$/g, '') || 'page';
const suffix = createHash('sha256').update(url.href).digest('hex').slice(0, 8);
return `${readable}-${suffix}.${extension}`;
}
const input = process.argv[2];
if (!input) throw new Error('Usage: node capture.mjs https://example.com/path');
const outputDir = path.resolve('screenshots');
await mkdir(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
const response = await page.goto(input, { waitUntil: 'load', timeout: 45_000 });
if (!response) throw new Error('Navigation did not return an HTTP response');
if (!response.ok()) {
throw new Error(`Navigation returned HTTP ${response.status()} ${response.statusText()}`);
}
// Replace this with a task-specific selector or readiness check if needed.
const finalUrl = page.url();
const outputPath = path.join(outputDir, filenameFromUrl(finalUrl));
await page.screenshot({ path: outputPath, fullPage: true, animations: 'disabled' });
console.log(`Saved ${outputPath} from ${finalUrl}`);
} finally {
await browser.close();
}
Run it with:
node capture.mjs 'https://docs.example.org/guides/page?campaign=spring#steps'
The example waits for the browser’s load event, which is a starting point, not proof that every application has finished rendering. For a page that fills in after load, wait for a known element or a task-specific state before taking the screenshot.
3. Derive a safe filename from the final URL
Use the parsed URL’s hostname and pathname as the readable portion. The example omits the query and fragment, normalizes punctuation to hyphens, trims the result, caps its length, and adds a deterministic hash. For the illustrative URL https://docs.example.org/guides/page?campaign=spring#steps, this policy produces a name shaped like docs-example-org-guides-page-<hash>.png. The exact policy is yours; Playwright does not provide a built-in URL-to-slug naming convention.
Query strings and fragments
Query values can contain tracking identifiers, session values, search terms, or personal data. Avoid copying them into filenames that may be uploaded, logged, or shared. If query parameters identify meaningful states in your application, allowlist only the specific keys and sanitize their values. Fragments often identify a section within a page and can also be omitted unless that distinction matters to the capture.
Collisions and overwrites
Different URLs can collapse to the same readable name after punctuation is removed or case is normalized. The hash suffix in the example distinguishes their complete URL strings in the usual case. If repeated captures of the same URL must both be retained, add a timestamp or job identifier. If repeated runs should replace an existing artifact, keep the deterministic name and define that overwrite behavior intentionally.
Filesystem and path rules
Do not use a raw URL as a path: slashes create directories, and reserved characters vary across filesystems. Keep generated names inside a known output directory, restrict extensions to supported formats, and never let untrusted input select an arbitrary directory. The Page API resolves relative screenshot paths against the current working directory. Playwright MCP documents relative filenames as resolving from the workspace root, so choose paths with the interface in mind.
4. Capture viewport, element, or full page
Viewport screenshot
Omit fullPage to capture the current viewport:
await page.screenshot({ path: outputPath });
Set the viewport before navigation or before capture to control the layout dimensions:
await page.setViewportSize({ width: 1280, height: 800 });
await page.screenshot({ path: outputPath });
Element screenshot
Use a locator for a component. Wait for it to be visible, and handle the case where the selector does not match:
const target = page.locator('[data-testid="pricing-table"]');
await target.waitFor({ state: 'visible', timeout: 10_000 });
await target.screenshot({ path: outputPath });
A stable test identifier is generally less fragile than a selector based on generated classes. If your agent supplies a selector, validate it against an allowed scope and make a missing or ambiguous target a clear task error.
Full-page screenshot
The main example uses fullPage: true. Playwright captures the full scrollable page, but a long or dynamically changing page may need additional handling. Lazy-loaded images can require scrolling through the page before capture; infinite-scroll pages may never reach a natural end. If completeness matters, use a site-specific scroll-and-check routine and verify the output rather than assuming navigation alone loaded every section.
5. Wait for the page state your task needs
There is no single readiness wait that guarantees a stable screenshot on every site. A navigation event can complete while client-side rendering, fonts, images, or data requests are still in progress. Pick a signal tied to the content being captured.
Wait for a known element
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
await page.locator('main h1').waitFor({ state: 'visible', timeout: 15_000 });
await page.screenshot({ path: outputPath, fullPage: true });
Wait for a specific application condition
await page.waitForFunction(() => {
return document.querySelector('[data-render-state="complete"]') !== null;
}, { timeout: 15_000 });
Use a short fixed delay only when the page offers no better signal, and keep it bounded. A delay adds time to every capture and still cannot prove that content is ready.
6. Use Playwright through an AI agent
An AI agent can decide which URL and target to capture, while the application owns validation, filename policy, and output permissions. This separation makes generated artifacts predictable and keeps the agent from turning arbitrary text into filesystem paths.
- Accept a URL and an optional capture scope from the agent.
- Parse and validate the URL; allow only
http:andhttps:unless your product has a specific need for other schemes. - Navigate the browser and verify the expected page or content marker.
- Read the final URL after redirects or agent interaction.
- Derive a sanitized name, apply collision policy, and resolve it under a controlled output directory.
- Capture the requested viewport, element, or full page and report both the final URL and saved path.
Playwright offers three relevant interfaces in the cited documentation: its programmatic Page API, a CLI command, and an MCP screenshot tool. The API is suitable when your application needs to own parsing and naming logic. The CLI accepts a filename option. The MCP tool accepts a filename and documents workspace-root path resolution. In each case, URL-derived naming remains application or agent workflow logic; confirm the exact interface’s options and path behavior in its documentation.
7. cURL, Python, and Node.js with Playwright MCP
The browser API example above is the complete local capture implementation. If your AI agent already connects to Playwright MCP, its screenshot tool can take the image and accept a filename. The MCP server is an installed process/tool integration, not an HTTP endpoint, so cURL is not a way to call the MCP screenshot tool directly. Keep the URL parsing and safe filename policy in your host application, then pass the calculated filename to the tool.
For an MCP client, the documented tool is browser_take_screenshot. A client call has the following shape; exact invocation syntax depends on your MCP SDK and connection setup:
await mcp.callTool({
name: 'browser_take_screenshot',
arguments: {
filename: 'screenshots/docs-example-org-guides-page.png',
fullPage: true
}
});
Playwright MCP supports screenshot format and scale controls as documented by Microsoft. PNG is the documented default when no format is specified. The CLI and MCP docs describe PNG, JPEG, and WebP; for code using the Page API, the output extension determines the image type. Use a filename extension matching the desired format and check the interface documentation for its accepted option names.
8. Or skip the browser setup
If you only need a screenshot from a URL and do not need to run browser automation code yourself, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF, and its parameters include full-page capture, element selection, viewport and device presets, custom CSS or JavaScript, and wait controls. The parameter names used by other screenshot APIs also work, which can make switching easier. 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,
)
r.raise_for_status()
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 import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; those steps can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month, with no card required.
9. Reliability, performance, and cost
- Browser startup: Reusing a browser process for a batch can avoid repeated launch overhead, but isolate pages or browser contexts when cookies and state must not leak between tasks.
- Timeouts: Bound navigation and selector waits. Report whether the timeout occurred during navigation, readiness, or screenshot capture so retries target the right stage.
- Retries: Retry transient navigation failures selectively. Do not blindly retry deterministic HTTP errors or a selector that does not exist; those can waste time and produce duplicate artifacts.
- Output size: Full-page images and high device-pixel scale can increase memory, storage, and transfer requirements. Choose the smallest scope and scale that preserves required detail.
- Reproducibility: Record the final URL, viewport, scope, and capture time alongside the path when the image is used for review or debugging. Dynamic content can make captures differ between runs.
- Local cost: Playwright itself does not charge per screenshot in this workflow, but your browser hosts, compute, storage, and development/operations time have costs. No benchmark or fixed capture time is implied here.
- Hosted capture cost: For ScreenshotNeo, the stated plans are Free: 1,000 shots/month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The image is blank or shows a loading state | The app renders content after the chosen navigation event | Wait for a task-specific visible element or application-ready marker; inspect the final page state. |
| The element screenshot times out | The selector is wrong, the element is hidden, or the page never reached its ready state | Check the selector against the rendered DOM, wait for visibility, and report a target-specific error. |
| Images are missing in a full-page capture | Images load lazily as they approach the viewport | Scroll through the page and wait for required images before capture; verify behavior for that site. |
| Two pages overwrite one file | Different URLs normalized to the same readable path | Add a deterministic URL hash or unique job suffix and define whether repeated captures overwrite. |
| Filename contains private or noisy values | Query parameters were copied directly into the name | Omit query and fragment by default; include only reviewed, allowlisted keys when necessary. |
| File appears in an unexpected directory | The relative path was resolved from a different working root | Use an absolute path under a configured output directory and account for API versus MCP path rules. |
| Screenshot is too large or slow to process | Full-page scope or device-pixel scaling exceeds the use case | Capture the viewport or target element, reduce scale, or resize after capture. |
| Navigation fails for a URL | Unsupported scheme, unreachable host, timeout, or HTTP error | Validate the scheme, surface status and timeout details, and retry only failures likely to be transient. |
11. Checklist before shipping
- Validate URLs with a URL parser and restrict protocols.
- Use hostname and pathname for readable names; omit sensitive query data by default.
- Normalize reserved characters, cap the filename length, preserve the intended image extension, and handle collisions.
- Resolve output inside a known directory and decide overwrite behavior.
- Choose viewport, element, or full-page scope intentionally.
- Wait for a page-specific readiness signal and bound every wait.
- Return the final redirected URL and output path to the agent or calling application.
- Check dynamic and lazy-loaded content when completeness matters.
FAQ
Does Playwright automatically name a screenshot from the page URL?
No built-in URL-derived naming behavior is established by the cited docs. Pass an explicit path and implement URL parsing and sanitization in your application.
Should the filename include query parameters?
Usually not. Query values may expose private information or tracking data. Allowlist only parameters that distinguish meaningful states in your workflow.
Can an AI agent choose a screenshot filename?
It can propose one, but validate and sanitize it in application code before writing. The same rule applies to agent-supplied URLs and selectors.
What format should I choose?
PNG is a practical default when preserving sharp text matters. The documented CLI and MCP interfaces also support JPEG and WebP; match the extension to the selected format.


