webshot#
Web page screenshots and HTML-to-PDF rendering with Playwright (Chromium). A JSON spec lists the pages;
the driver logs in once per auth profile, runs optional clicks and fills, hides elements, and saves one
PNG per page: the viewport, the whole page, or a single element. webshot_pdf prints an HTML file to PDF
with backgrounds and an optional page-number footer.
Playwright runs from a Python venv kept outside the repository
(${XDG_CACHE_HOME:-$HOME/.cache}/nr-webshot/venv). webshot_ensure creates it. Import with
shlib_import python webshot; logging is optional.
Exit codes for every function: 0 ok, 1 the run failed (login refused, selector not found, page error),
2 bad arguments or spec, 3 Python or Playwright unavailable (for webshot_android_audit: adb, Python or a
ready device).
Functions#
webshot_venv_dir- Purpose: Print the venv directory webshot uses.
- Returns: 0.
- Env:
WEBSHOT_VENVoverrides the default${XDG_CACHE_HOME:-$HOME/.cache}/nr-webshot/venv. -
Example:
ls "$(webshot_venv_dir)/bin" -
webshot_python - Purpose: Print a Python interpreter that can import Playwright.
- Returns: 0 and the path; 3 when none is found.
- Env:
WEBSHOT_PYTHON, when set, is the only candidate. Otherwise the webshot venv, thenpython3. -
Example:
py="$(webshot_python)" || webshot_ensure -
webshot_ensure [venv_dir] - Purpose: Create the venv if needed and install the pinned
playwrightpackage and its Chromium. Re-running is cheap: it returns as soon as Playwright imports and Chromium is on disk. - Args:
venv_dirdefaults towebshot_venv_dir. - Returns: 0 and the venv Python path; 3 when Python 3.9+, pip or the browser download is unavailable.
- Env:
WEBSHOT_PLAYWRIGHT_VERSION(default1.63.0). Browsers go to Playwright's own cache (~/.cache/ms-playwrighton Linux). - Dependencies: the
pythonmodule; network access on first run. -
Example:
webshot_ensure >/dev/null -
webshot_capture spec.json out_dir - Purpose: Capture every shot in the spec to
out_dir/<name>.pngand writeout_dir/manifest.json. - Args: path to a spec file (format below) and an output directory, created if missing.
- Returns: 0; 1 when a login, navigation, action or selector fails; 2 for a missing or invalid spec; 3 without Playwright.
- Env:
${VAR}references inbase_url, auth profiles, shotpath/urland actionvalues are expanded from the environment, so credentials and IDs stay out of spec files. Selectors andevalcode are never expanded, so a JavaScript template literal is left as written. -
Example:
APP_PASSWORD=... webshot_capture docs/shots.json build/shots -
webshot_audit start_url [--device NAME]... [--skip PREFIX]... [--limit N] [--shots DIR] [--timeout-ms N] - Purpose: Crawl a site on phone devices and list pages wider than the screen. Follows links on the
same scheme and host as the page
start_urlends on after redirects (fragments dropped; a redirect is reported once, under the URL it ends on) up to--limitpages (default 500), and loads each page on every--device(Playwright device names; defaultiPhone 13andGalaxy S8, 390 and 360px). A page fails on a device when the document is wider than the viewport, or when anh1-h3is wider than its box: cut off by a hidden overflow, which does not widen the page. For a wide page,elementsnames up to five of the deepest elements whose content overflows, such as a word that cannot wrap.--skipdrops URLs whose path starts withPREFIX(repeatable, e.g.--skip /admin --skip /api/).--shots DIRsavesDIR/<path>--<device>.pngper page and device (a long path is cut and keeps a hash). - Output: stdout JSON
{"crawled": N, "findings": {url: {device: {width, scroll_width, elements, clipped_headings}}}, "http_errors": {url: status or error}, "downloads": [url]}. A page that fails to load is listed inhttp_errorsand is not a finding. A link the browser downloads (a PDF, a zip) is listed indownloadsand is not counted incrawled; so is a non-HTML file served over HTTP that the browser shows itself, such as an image (by itsContent-Type). - Returns: 0 no finding; 1 a finding, or the start page did not load; 2 bad arguments or an unknown device; 3 without Playwright.
- Env: none.
-
Example:
webshot_audit http://localhost:8080/ --skip /admin --shots build/phone > build/audit.json -
webshot_android_audit url [--serial S] [--path P]... [--shots DIR] [--timeout-ms N] - Purpose: Run the
webshot_auditchecks in Chrome on a real Android phone attached over adb (USB, oradb tcpip). The phone needs USB debugging on and Chrome installed. Alocalhostor127.0.0.1URL reaches the phone throughadb reverse tcp:PORT tcp:PORT, so a local site is never opened to the network; any other host is opened as given. Chrome opens the URL and the checks run through its DevTools socket (adb forward tcp:0 localabstract:chrome_devtools_remote), with Python's standard library: Playwright does not support CDP to Chrome on Android, and Playwright is not needed.--path(repeatable) checks those paths ofurlinstead ofurlitself; there is no crawl.--shots DIRsavesDIR/<path>--<model>.pngfromadb exec-out screencap, so the PNG is the whole screen with Chrome's toolbar.--serialpicks a device when more than one is attached. Both port forwards are removed on exit, also on INT and TERM; a reverse that existed before the run is left in place. - Output: stdout JSON
{"device": model, "viewport": {width, height, dpr}, "checked": [url], "findings": {url: {model: {width, scroll_width, elements, clipped_headings}}}, "http_errors": {url: status or error}}. - Returns: 0 no finding; 1 a finding, or a page did not load; 2 bad arguments (a URL with a quote or space too: it passes through the phone's shell); 3 no adb, no Python, or no ready device.
- Env: none.
-
Example:
webshot_android_audit http://localhost:8080/ --path / --path /calendar --shots build/phone > build/phone.json -
webshot_pdf input.html output.pdf [--format Letter|A4|A3|Legal|Tabloid] [--landscape] [--footer TEXT] [--wait-ms N] - Purpose: Print a local HTML file to PDF with backgrounds. CSS
@pagesize and margins win when the document sets them.--footeraddsTEXTandpage / totalat the bottom of every page. - Returns: 0; 1 when rendering fails; 2 for bad arguments or a missing input; 3 without Playwright.
- Env: none.
- Example:
webshot_pdf build/report.html build/report.pdf --footer "Quarterly report"
Spec format#
{
"base_url": "http://localhost:8080",
"viewport": {"width": 1440, "height": 900},
"device_scale_factor": 2,
"wait_ms": 600,
"timeout_ms": 15000,
"hide": ["[data-dev-banner]"],
"fail_on_console_error": false,
"auth": {
"editor": {
"type": "api_token",
"url": "/api/login",
"body": {"email": "editor@example.com", "password": "${APP_PASSWORD}"},
"token_path": "access_token",
"storage_key": "token"
},
"viewer": {
"type": "form",
"url": "/login",
"steps": [
{"fill": "#email", "value": "viewer@example.com"},
{"fill": "#password", "value": "${APP_PASSWORD}"},
{"click": "button[type=submit]"},
{"wait_for": "nav"}
]
}
},
"shots": [
{"name": "home", "path": "/"},
{"name": "dashboard", "path": "/dashboard", "auth": "editor", "full_page": true},
{"name": "totals-card", "path": "/dashboard", "auth": "editor",
"selector": "[data-shot=totals]", "padding": 8},
{"name": "edit-dialog", "path": "/items/42", "auth": "editor",
"actions": [{"click": "text=Edit"}, {"wait_for": "[role=dialog]"}]},
{"name": "home-mobile", "path": "/", "viewport": {"width": 390, "height": 844}}
]
}
Top-level keys (all optional except shots):
| Key | Meaning |
|---|---|
base_url |
Joined with each shot's path and with relative auth URLs. Required when a shot uses path. |
viewport, device_scale_factor |
Defaults for every shot. Use a factor of 2 for print-sharp images. |
device |
A Playwright device name ("iPhone 13", "Pixel 7", "Desktop Chrome"): viewport, scale factor, user agent, touch and mobile layout for every shot. An unknown name exits 2 and lists the known ones. The device's browser type is ignored; shots always run in Chromium. |
wait_ms |
Pause after load and actions, before the capture (default 500). |
wait_until |
Playwright load state for navigation (default networkidle). |
timeout_ms |
Timeout for each locator and action (default 15000). |
hide |
CSS selectors set to display: none on every shot. |
color_scheme, locale, timezone_id |
Browser context options. |
fail_on_console_error |
Exit 1 when any page logs a console error (default: record it in the manifest only). |
auth |
Named profiles, used by shot.auth. |
Auth profiles:
none: no login.api_token: sendsmethod(defaultPOST) tourlwith the JSONbodyand optionalheaders, reads the value attoken_path(dot path, list indexes allowed) and writes it tolocalStorage[storage_key]before any page script runs. Logs in once per profile.form: opensurl, runssteps(same actions as shots), then reuses the resulting cookies and storage for every shot with that profile.
Shot keys: name (required; letters, digits, ., _, -), path or url, auth, device, viewport,
device_scale_factor, full_page, selector (Playwright locator; captures that element),
padding (pixels around a selector capture), wait_for (selector that must be visible first),
wait_ms, hide, actions, title (copied to the manifest, handy for captions).
Shot keys beat spec keys. At the same level, an explicit viewport or device_scale_factor beats the
device's, so {"device": "Pixel 7", "viewport": {"width": 360, "height": 640}} is a 360px phone with touch
and a mobile user agent. With device, a page without <meta name="viewport"> lays out at 980px, as on
a real phone.
Actions, run in order: {"click": sel}, {"fill": sel, "value": text}, {"select": sel, "value": v},
{"press": key, "selector": sel} (selector optional), {"hover": sel}, {"scroll_to": sel},
{"wait_for": sel}, {"wait_ms": n}, {"eval": "() => ..."}. An eval that throws fails the run,
which makes it a cheap assertion.
manifest.json lists every shot: name, file, width, height (pixels), url, device (empty without one), title,
console_errors, http_errors (responses with status 400 or more).
Command line#
bin/webshot ensure | capture <spec> <out_dir> | pdf <in.html> <out.pdf> [options] wraps the functions
above with the same exit codes.