Skip to content

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_VENV overrides 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, then python3.
  • Example: py="$(webshot_python)" || webshot_ensure

  • webshot_ensure [venv_dir]

  • Purpose: Create the venv if needed and install the pinned playwright package and its Chromium. Re-running is cheap: it returns as soon as Playwright imports and Chromium is on disk.
  • Args: venv_dir defaults to webshot_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 (default 1.63.0). Browsers go to Playwright's own cache (~/.cache/ms-playwright on Linux).
  • Dependencies: the python module; 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>.png and write out_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 in base_url, auth profiles, shot path/url and action values are expanded from the environment, so credentials and IDs stay out of spec files. Selectors and eval code 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_url ends on after redirects (fragments dropped; a redirect is reported once, under the URL it ends on) up to --limit pages (default 500), and loads each page on every --device (Playwright device names; default iPhone 13 and Galaxy S8, 390 and 360px). A page fails on a device when the document is wider than the viewport, or when an h1-h3 is wider than its box: cut off by a hidden overflow, which does not widen the page. For a wide page, elements names up to five of the deepest elements whose content overflows, such as a word that cannot wrap. --skip drops URLs whose path starts with PREFIX (repeatable, e.g. --skip /admin --skip /api/). --shots DIR saves DIR/<path>--<device>.png per 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 in http_errors and is not a finding. A link the browser downloads (a PDF, a zip) is listed in downloads and is not counted in crawled; so is a non-HTML file served over HTTP that the browser shows itself, such as an image (by its Content-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_audit checks in Chrome on a real Android phone attached over adb (USB, or adb tcpip). The phone needs USB debugging on and Chrome installed. A localhost or 127.0.0.1 URL reaches the phone through adb 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 of url instead of url itself; there is no crawl. --shots DIR saves DIR/<path>--<model>.png from adb exec-out screencap, so the PNG is the whole screen with Chrome's toolbar. --serial picks 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 @page size and margins win when the document sets them. --footer adds TEXT and page / total at 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: sends method (default POST) to url with the JSON body and optional headers, reads the value at token_path (dot path, list indexes allowed) and writes it to localStorage[storage_key] before any page script runs. Logs in once per profile.
  • form: opens url, runs steps (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.