<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vitalii Holben</title>
    <description>The latest articles on DEV Community by Vitalii Holben (@webmox).</description>
    <link>https://dev.to/webmox</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3842314%2F73646452-030c-4c9e-8633-79bbc79167fe.jpg</url>
      <title>DEV Community: Vitalii Holben</title>
      <link>https://dev.to/webmox</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/webmox"/>
    <language>en</language>
    <item>
      <title>How to take screenshots of password-protected pages with a screenshot API</title>
      <dc:creator>Vitalii Holben</dc:creator>
      <pubDate>Fri, 10 Apr 2026 13:07:05 +0000</pubDate>
      <link>https://dev.to/webmox/how-to-take-screenshots-of-password-protected-pages-with-a-screenshot-api-pmc</link>
      <guid>https://dev.to/webmox/how-to-take-screenshots-of-password-protected-pages-with-a-screenshot-api-pmc</guid>
      <description>&lt;p&gt;Not every page you need to screenshot is open to the world. Sometimes you need a screenshot of a service admin panel, an internal dashboard, a staging server, or a page behind basic auth. And that's where the problem starts: you send the URL to a screenshot API, and what comes back is a screenshot of the login form. The headless browser on the API side doesn't know your credentials and visits the page as a brand new user.&lt;/p&gt;
&lt;p&gt;I want to walk through how to handle this. As an example, I'll use my own project fixheaders.com, a security headers scanner. It has a Filament admin panel, and I periodically need a screenshot of the dashboard showing scan counts without logging in manually every time.&lt;/p&gt;
&lt;p&gt;I'll cover three authentication methods: cookies, custom HTTP headers, and Basic Auth. Each method comes with code examples in cURL, Node.js, and PHP.&lt;/p&gt;
&lt;h2&gt;Why a regular screenshot request won't work&lt;/h2&gt;
&lt;p&gt;When you ask a screenshot API (or &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/blog/screenshot-api-vs-puppeteerplaywright-when-to-build-and-when-to-buy"&gt;Playwright, or Puppeteer&lt;/a&gt;) to capture a URL, the headless browser opens it as a completely new visitor. No session, no cookies, no saved credentials. If the page requires a login, the server sees an unauthenticated request and redirects to the login form. Or just shows it directly.&lt;/p&gt;
&lt;p&gt;Here's what happens when you send a regular request to the fixheaders.com admin URL without any cookies:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST https://screenshotrun.com/api/v1/screenshots \
  -H "Authorization: Bearer $SCREENSHOTRUN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://fixheaders.com/admin"}'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FLtbgkhmeYPuYCfLLT6OFrELpcIXQmSkJ0Ycfojm0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FLtbgkhmeYPuYCfLLT6OFrELpcIXQmSkJ0Ycfojm0.png" alt="API response with id and status: pending" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We wait a few seconds and download the result:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -H "Authorization: Bearer $SCREENSHOTRUN_KEY" \
  https://screenshotrun.com/api/v1/screenshots/SCREENSHOT_ID/image \
  -o screenshot-no-auth.png&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FlG11n6hGLgE5cRBYJomTqSW14rOBXZA3oqr5GjBo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FlG11n6hGLgE5cRBYJomTqSW14rOBXZA3oqr5GjBo.png" alt="terminal download command" width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Open the file and you see this:&lt;br&gt;&lt;br&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FZKAupWI3096qJgdh17oc0CgZdJ0xTeRo4XsBLZRW.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FZKAupWI3096qJgdh17oc0CgZdJ0xTeRo4XsBLZRW.png" alt="FixHeaders login page" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The login form. This is exactly what the headless browser sees when it visits a protected URL without authentication. It's the same thing that happens when you open a private URL in an incognito window. The browser has no cookies, so the server treats you as a stranger.&lt;/p&gt;
&lt;p&gt;The fix is to give the headless browser the same credentials your regular browser already has. There are a few ways to do that, depending on how the target site handles authentication.&lt;/p&gt;
&lt;h2&gt;Method 1: pass session cookies (the most common case)&lt;/h2&gt;
&lt;p&gt;Most web apps (Laravel, Django, Rails, WordPress, Filament, any admin panel) use cookie-based sessions. When you log in, the server creates a session and sends a cookie. Every subsequent request includes that cookie, and the server knows who you are.&lt;/p&gt;
&lt;p&gt;The idea is simple: grab the session cookie from your browser and pass it to the screenshot API. The headless browser will send that cookie with the request, the server will see a valid session, and you'll get a screenshot of the actual dashboard, not the login page.&lt;/p&gt;
&lt;h3&gt;Step 1: find your session cookie&lt;/h3&gt;
&lt;p&gt;Open the site you want to screenshot (in my case, the fixheaders.com admin panel). Log in normally. Then open DevTools → Application → Cookies (in Chrome) or Storage → Cookies (in Firefox).&lt;br&gt;&lt;br&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FRp3bjRBPuMISk3OFcdYJLbWtVEgOnsbHEipy7xT3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FRp3bjRBPuMISk3OFcdYJLbWtVEgOnsbHEipy7xT3.png" alt="DevTools cookies tab" width="800" height="719"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Look for the session cookie. In Laravel apps it's usually called &lt;code&gt;laravel_session&lt;/code&gt; or a custom name from &lt;code&gt;config/session.php&lt;/code&gt;. In my case it's &lt;code&gt;fixheaders-session&lt;/code&gt;. The name varies across frameworks. In some apps it's &lt;code&gt;PHPSESSID&lt;/code&gt;, in Django it's &lt;code&gt;sessionid&lt;/code&gt;, in Rails it's &lt;code&gt;_yourapp_session&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Copy the cookie value. It'll look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eyJpdiI6IkxMNk1DVjZhN0FKWjZ2a3...long_base64_string&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: pass it to the screenshot API&lt;/h3&gt;
&lt;p&gt;Here's how to send that cookie with a screenshotrun API request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;cURL:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST https://screenshotrun.com/api/v1/screenshots \
  -H "Authorization: Bearer $SCREENSHOTRUN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://fixheaders.com/admin",
    "cookies": [
      {
        "name": "fixheaders-session",
        "value": "eyJpdiI6IkxMNk1DVjZhN0FKWjZ2a3...",
        "domain": "fixheaders.com"
      }
    ]
  }'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FMMt32B3QDT6jzaWYl2uoLMh9IjNy29yIhpEb4a80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FMMt32B3QDT6jzaWYl2uoLMh9IjNy29yIhpEb4a80.png" alt="terminal with cookie JSON" width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;cookies&lt;/code&gt; parameter takes an array of objects. Each one needs a &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;value&lt;/code&gt;, and &lt;code&gt;domain&lt;/code&gt;. The domain tells the browser which site to send the cookie to. Without it, the cookie might not get attached to the request.&lt;/p&gt;
&lt;p&gt;Download the result the same way, using the ID from the response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -H "Authorization: Bearer $SCREENSHOTRUN_KEY" \
  https://screenshotrun.com/api/v1/screenshots/SCREENSHOT_ID/image \
  -o screenshot-with-auth.png&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here's what we get this time:&lt;br&gt;&lt;br&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FJKRYtPs6g9lrZ6bqQvJ5ep08w6NJHNpGWopdL9mo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FJKRYtPs6g9lrZ6bqQvJ5ep08w6NJHNpGWopdL9mo.png" alt="FixHeaders dashboard with stats" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The actual dashboard. Scan counts, average score, recent checks. Exactly what I see when I'm logged in through the browser. One cookie made all the difference.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Node.js:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const response = await fetch("https://screenshotrun.com/api/v1/screenshots", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://fixheaders.com/admin",
    cookies: [
      {
        name: "fixheaders-session",
        value: "eyJpdiI6IkxMNk1DVjZhN0FKWjZ2a3...",
        domain: "fixheaders.com",
      },
    ],
  }),
});

const { data } = await response.json();
console.log(data.id);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;PHP (Laravel):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you're working with PHP and haven't set up the screenshot API yet, I covered the full setup process in &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/blog/take-website-screenshot-php"&gt;the PHP screenshot tutorial&lt;/a&gt;. Here's the short version for authenticated pages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$response = Http::withToken(env('SCREENSHOTRUN_API_KEY'))
    -&amp;gt;post('https://screenshotrun.com/api/v1/screenshots', [
        'url' =&amp;gt; 'https://fixheaders.com/admin',
        'cookies' =&amp;gt; [
            [
                'name' =&amp;gt; 'fixheaders-session',
                'value' =&amp;gt; 'eyJpdiI6IkxMNk1DVjZhN0FKWjZ2a3...',
                'domain' =&amp;gt; 'fixheaders.com',
            ],
        ],
    ]);

$screenshotId = $response-&amp;gt;json('data.id');&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the request completes, poll the screenshot status or use a &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/docs/webhooks"&gt;webhook&lt;/a&gt; to get notified when it's ready.&lt;/p&gt;
&lt;h3&gt;What can go wrong with cookies&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Session expiration.&lt;/strong&gt; This is the biggest one. Session cookies have a lifetime. In Laravel the default is 2 hours. After that, the session is invalid and the screenshot API will get the login page again. If you're automating this, you need to refresh the cookie periodically. One option is to write a script that logs in via HTTP (POST to the login endpoint with credentials), grabs the fresh session cookie from the &lt;code&gt;Set-Cookie&lt;/code&gt; response header, and uses that for the screenshot request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multiple cookies.&lt;/strong&gt; Some apps require more than one cookie. CSRF tokens, "remember me" cookies, or multi-cookie session setups. If the screenshot comes back as a login page even though you're sending the session cookie, check whether the site sets additional cookies during login. You can pass up to 20 cookies in a single screenshotrun request, which covers pretty much any setup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HttpOnly and Secure flags.&lt;/strong&gt; Session cookies are often marked as &lt;code&gt;HttpOnly&lt;/code&gt; (can't be read by JavaScript) and &lt;code&gt;Secure&lt;/code&gt; (only sent over HTTPS). These flags don't affect the screenshot API. The headless browser handles them the same way a regular browser does. Just make sure you're using HTTPS URLs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SameSite cookies.&lt;/strong&gt; If the cookie has &lt;code&gt;SameSite=Strict&lt;/code&gt;, it might not get sent in certain cross-origin scenarios. With a screenshot API, the headless browser navigates directly to the URL (it's a top-level navigation, not a cross-origin fetch), so &lt;code&gt;SameSite=Strict&lt;/code&gt; cookies are included normally.&lt;/p&gt;
&lt;h2&gt;Method 2: custom HTTP headers (API tokens and Bearer auth)&lt;/h2&gt;
&lt;p&gt;Not every protected page uses cookies. Internal tools, API documentation portals, and headless CMS interfaces sometimes protect pages with custom HTTP headers: an API token, a Bearer token, or a custom &lt;code&gt;X-Auth-Token&lt;/code&gt; header.&lt;/p&gt;
&lt;p&gt;If the page you need to screenshot checks for a specific header instead of (or in addition to) cookies, you can pass custom headers through the screenshot API.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;cURL:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST https://screenshotrun.com/api/v1/screenshots \
  -H "Authorization: Bearer $SCREENSHOTRUN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://internal-tool.example.com/dashboard",
    "headers": {
      "X-Auth-Token": "your-internal-token-here"
    }
  }'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Node.js:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const response = await fetch("https://screenshotrun.com/api/v1/screenshots", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://internal-tool.example.com/dashboard",
    headers: {
      "X-Auth-Token": "your-internal-token-here",
    },
  }),
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;PHP:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$response = Http::withToken(env('SCREENSHOTRUN_API_KEY'))
    -&amp;gt;post('https://screenshotrun.com/api/v1/screenshots', [
        'url' =&amp;gt; 'https://internal-tool.example.com/dashboard',
        'headers' =&amp;gt; [
            'X-Auth-Token' =&amp;gt; 'your-internal-token-here',
        ],
    ]);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;headers&lt;/code&gt; parameter accepts an object where keys are header names and values are header values. You can send up to 20 custom headers per request.&lt;/p&gt;
&lt;p&gt;One thing to watch out for: the &lt;code&gt;Authorization&lt;/code&gt; header in the &lt;code&gt;headers&lt;/code&gt; parameter is the one sent to the &lt;strong&gt;target page&lt;/strong&gt;, not to the screenshotrun API itself. The API key for authenticating with screenshotrun goes in the top-level &lt;code&gt;Authorization&lt;/code&gt; header of the HTTP request. The &lt;code&gt;headers&lt;/code&gt; object is what the headless browser sends when loading the target URL. These are two different things, and it's easy to mix them up.&lt;/p&gt;
&lt;h2&gt;Method 3: Basic Auth for staging and htpasswd-protected sites&lt;/h2&gt;
&lt;p&gt;If you've ever password-protected a staging server with Nginx's &lt;code&gt;auth_basic&lt;/code&gt; or Apache's &lt;code&gt;.htpasswd&lt;/code&gt;, you know how it works. The browser shows a modal dialog asking for a username and password. Headless browsers can't interact with that dialog, so the request just fails or returns a 401.&lt;/p&gt;
&lt;p&gt;The fix is simple: send the credentials as a Basic Auth header. The browser won't see the dialog at all because the server gets the credentials before it has a chance to ask for them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;cURL:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST https://screenshotrun.com/api/v1/screenshots \
  -H "Authorization: Bearer $SCREENSHOTRUN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://staging.example.com",
    "headers": {
      "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
    }
  }'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Basic dXNlcm5hbWU6cGFzc3dvcmQ=&lt;/code&gt; part is just &lt;code&gt;username:password&lt;/code&gt; encoded in Base64. You can generate it from the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo -n "username:password" | base64
# Output: dXNlcm5hbWU6cGFzc3dvcmQ=&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Node.js:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const credentials = Buffer.from("username:password").toString("base64");

const response = await fetch("https://screenshotrun.com/api/v1/screenshots", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://staging.example.com",
    headers: {
      "Authorization": `Basic ${credentials}`,
    },
  }),
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;PHP:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$credentials = base64_encode('username:password');

$response = Http::withToken(env('SCREENSHOTRUN_API_KEY'))
    -&amp;gt;post('https://screenshotrun.com/api/v1/screenshots', [
        'url' =&amp;gt; 'https://staging.example.com',
        'headers' =&amp;gt; [
            'Authorization' =&amp;gt; 'Basic ' . $credentials,
        ],
    ]);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This method works for any site that uses HTTP Basic Authentication. It's common on staging environments, dev servers, and some internal tools. If you're monitoring your staging after deployments, I wrote a separate article about &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/blog/when-a-screenshot-tells-you-what-a-log-cant-5-situations-that-matter"&gt;why screenshots catch things logs miss&lt;/a&gt; that covers the reasoning behind this.&lt;/p&gt;
&lt;h2&gt;Combining cookies with other capture options&lt;/h2&gt;
&lt;p&gt;All three authentication methods work alongside the regular screenshot parameters. You can take a full-page screenshot of an authenticated dashboard in dark mode, at a mobile viewport, with cookie banners blocked. All in one request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST https://screenshotrun.com/api/v1/screenshots \
  -H "Authorization: Bearer $SCREENSHOTRUN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://fixheaders.com/admin",
    "cookies": [
      {
        "name": "fixheaders-session",
        "value": "your-session-cookie-value",
        "domain": "fixheaders.com"
      }
    ],
    "full_page": true,
    "dark_mode": true,
    "device": "mobile",
    "format": "webp"
  }'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I covered the full list of parameters (dark mode, device presets, &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/blog/screenshot-specific-element-css-selector"&gt;CSS injection and element selectors&lt;/a&gt;) in the &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/docs/screenshots"&gt;API documentation&lt;/a&gt;. If you haven't looked at the other capture options yet, the &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/blog/how-to-take-website-screenshots-with-curl"&gt;cURL examples article&lt;/a&gt; walks through each one with copy-paste commands.&lt;/p&gt;
&lt;h2&gt;Automating the login (when cookies expire too fast)&lt;/h2&gt;
&lt;p&gt;If you want to capture authenticated screenshots on a schedule (say, every hour for a dashboard you embed somewhere), manually copying cookies won't cut it. Sessions expire, and your screenshot will revert to a login page.&lt;/p&gt;
&lt;p&gt;The workaround is to automate the login step itself. Instead of grabbing cookies from a browser by hand, write a script that logs in programmatically, extracts the session cookie, and passes it to the screenshot API.&lt;/p&gt;
&lt;p&gt;Here's a Node.js example using plain &lt;code&gt;fetch&lt;/code&gt; (if you need more context on the Node.js setup, check out &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/blog/how-to-take-a-website-screenshot-with-nodejs"&gt;the full Node.js tutorial&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function getSessionCookie(loginUrl, email, password) {
  const response = await fetch(loginUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password }),
    redirect: "manual",
  });

  const setCookie = response.headers.get("set-cookie");
  if (!setCookie) {
    throw new Error("No Set-Cookie header in login response");
  }

  const match = setCookie.match(/(\w+[-_]session)=([^;]+)/);
  if (!match) {
    throw new Error("Could not find session cookie in Set-Cookie header");
  }

  return { name: match[1], value: match[2] };
}

// Usage
const cookie = await getSessionCookie(
  "https://fixheaders.com/login",
  "your@email.com",
  "your-password"
);

const response = await fetch("https://screenshotrun.com/api/v1/screenshots", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://fixheaders.com/admin",
    cookies: [
      {
        name: cookie.name,
        value: cookie.value,
        domain: "fixheaders.com",
      },
    ],
  }),
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The login endpoint depends on your app. Laravel's default route is &lt;code&gt;POST /login&lt;/code&gt; with &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; fields (plus a CSRF token, more on that below). Django uses &lt;code&gt;POST /accounts/login/&lt;/code&gt;. WordPress uses &lt;code&gt;POST /wp-login.php&lt;/code&gt; with &lt;code&gt;log&lt;/code&gt; and &lt;code&gt;pwd&lt;/code&gt; fields.&lt;/p&gt;
&lt;h3&gt;The CSRF token problem&lt;/h3&gt;
&lt;p&gt;Most frameworks protect login forms with CSRF tokens. If you just POST to &lt;code&gt;/login&lt;/code&gt; without a valid token, you'll get a 419 (Laravel) or 403 (Django/Rails). You need to first GET the login page, extract the CSRF token from the HTML or cookies, then include it in the POST request.&lt;/p&gt;
&lt;p&gt;In Laravel, the CSRF token lives in a cookie called &lt;code&gt;XSRF-TOKEN&lt;/code&gt; (URL-encoded) and needs to be sent back as an &lt;code&gt;X-XSRF-TOKEN&lt;/code&gt; header or as a &lt;code&gt;_token&lt;/code&gt; form field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function laravelLogin(baseUrl, email, password) {
  // Step 1: GET the login page to grab the CSRF cookie
  const loginPage = await fetch(`${baseUrl}/login`);
  const csrfCookie = loginPage.headers
    .get("set-cookie")
    ?.match(/XSRF-TOKEN=([^;]+)/)?.[1];

  if (!csrfCookie) {
    throw new Error("Could not extract CSRF token");
  }

  const csrfToken = decodeURIComponent(csrfCookie);

  // Step 2: POST login credentials with the CSRF token
  const response = await fetch(`${baseUrl}/login`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-XSRF-TOKEN": csrfToken,
      Cookie: `XSRF-TOKEN=${csrfCookie}`,
    },
    body: JSON.stringify({ email, password }),
    redirect: "manual",
  });

  // Step 3: extract the session cookie from the response
  const sessionCookie = response.headers
    .get("set-cookie")
    ?.match(/(\w+[-_]session)=([^;]+)/);

  if (!sessionCookie) {
    throw new Error("Login failed — no session cookie returned");
  }

  return { name: sessionCookie[1], value: sessionCookie[2] };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is more involved than just copying a cookie from DevTools. But it runs unattended, and the session is always fresh. I use a similar approach inside screenshotrun's own renderer when I need to test authenticated pages during development.&lt;/p&gt;
&lt;h2&gt;When to use which method&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Cookies&lt;/strong&gt; — for any standard web app with form-based login. This is the most common case: admin panels, dashboards, CMS backends, SaaS tools. If you log in through a form in your browser and the site remembers you via a cookie, this is your method.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Custom headers&lt;/strong&gt; — for internal tools and API-protected pages. If the page checks for an &lt;code&gt;X-Auth-Token&lt;/code&gt;, a Bearer token, or any non-standard header, use the &lt;code&gt;headers&lt;/code&gt; parameter. This also works for API documentation portals that require authentication headers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Basic Auth&lt;/strong&gt; — for staging environments and htpasswd-protected sites. If the browser shows a native username/password dialog (not an HTML form), the site uses HTTP Basic Authentication. Pass the credentials as a Base64-encoded &lt;code&gt;Authorization&lt;/code&gt; header.&lt;/p&gt;
&lt;p&gt;Some setups combine multiple methods. A staging server might have Basic Auth at the Nginx level and cookie-based sessions at the application level. In that case, pass both — the &lt;code&gt;headers&lt;/code&gt; parameter with the Basic Auth header and the &lt;code&gt;cookies&lt;/code&gt; parameter with the session cookie. The screenshotrun API sends everything together with the request.&lt;/p&gt;
&lt;h2&gt;Security notes&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Don't hardcode credentials.&lt;/strong&gt; Use environment variables or a secrets manager. If you're storing session cookies or passwords in your codebase, you're one accidental commit away from leaking them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use dedicated accounts.&lt;/strong&gt; If you're automating dashboard screenshots, create a read-only user account specifically for this purpose. Don't use your admin credentials in scripts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Be careful with screenshot storage.&lt;/strong&gt; Authenticated dashboards might contain sensitive data: revenue numbers, user emails, internal metrics. If you're using screenshot &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/blog/how-to-cache-screenshots"&gt;caching&lt;/a&gt; or storing images long-term, think about who has access to those files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Session cookies are temporary credentials.&lt;/strong&gt; Treat them like passwords. Don't log them, don't put them in URLs, don't send them over unencrypted connections.&lt;/p&gt;
&lt;h2&gt;What I ended up doing with fixheaders&lt;/h2&gt;
&lt;p&gt;Back to the original problem. I needed to periodically check fixheaders.com scan stats without logging in manually every time.&lt;/p&gt;
&lt;p&gt;Here's what I built: a scheduled task in Laravel (&lt;code&gt;schedule:run&lt;/code&gt;) that fires every hour. It logs into fixheaders using the automated approach above, takes a screenshot of the dashboard, and saves it. I can open the latest screenshot any time and see the numbers without any extra steps.&lt;/p&gt;
&lt;p&gt;Could I have built a proper API integration instead? Sure. But that would have taken way longer than the 20 minutes this approach took. Sometimes a screenshot is the fastest way to pull data out of a system that doesn't have an API for what you need.&lt;/p&gt;
&lt;p&gt;If you want to try screenshotrun for something like this, the &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/register"&gt;free plan&lt;/a&gt; gives you 200 screenshots per month, more than enough for hourly dashboard captures. The &lt;code&gt;cookies&lt;/code&gt; and &lt;code&gt;headers&lt;/code&gt; parameters are available on the &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/pricing"&gt;Pro plan&lt;/a&gt; and above.&lt;/p&gt;
&lt;p&gt;I hope this saves you some time next time you need to screenshot a page behind a login. If you hit edge cases with specific frameworks (NextAuth, Passport, Sanctum — they all have their quirks), feel free to &lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com/contact-us"&gt;reach out&lt;/a&gt; and I'll try to help.&lt;/p&gt;

</description>
      <category>api</category>
      <category>javascript</category>
      <category>node</category>
      <category>playwright</category>
    </item>
    <item>
      <title>How to take a website screenshot with Python</title>
      <dc:creator>Vitalii Holben</dc:creator>
      <pubDate>Mon, 30 Mar 2026 19:55:57 +0000</pubDate>
      <link>https://dev.to/webmox/how-to-take-a-website-screenshot-with-python-5dic</link>
      <guid>https://dev.to/webmox/how-to-take-a-website-screenshot-with-python-5dic</guid>
      <description>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;Today I want to walk you through a step-by-step guide on how to capture website screenshots using Python. At first I thought this would take me a good week to put together and test everything properly. Turned out to be much simpler than I expected, especially when using a third-party API. But more on that later.&lt;/p&gt;
&lt;p&gt;Python gives you three solid options: Selenium, Playwright, and a screenshot API. Each one covers a different use case and has its own limits. In this guide I'll go through all three, show you working code you can run right now, and explain when each approach makes sense.&lt;/p&gt;
&lt;h2&gt;What you'll need&lt;/h2&gt;
&lt;p&gt;Before we start, make sure you have Python 3.8 or higher installed. Check your version:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python --version&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If Python is in place, let's create a working directory and a virtual environment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir python-screenshots&lt;br&gt;
cd python-screenshots&lt;br&gt;
python -m venv venv&lt;br&gt;
source venv/bin/activate  # Linux/macOS
&lt;h1&gt;
  
  
  or venv\Scripts\activate on Windows&lt;/h1&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The virtual environment keeps your global packages clean. Now we can install libraries and write code.&lt;/p&gt;
&lt;h2&gt;Method 1: Selenium&lt;/h2&gt;
&lt;p&gt;Selenium is an old, battle-tested tool for browser automation. If you already use it for testing or scraping, adding screenshots to your existing code is the easiest path. No need to pull in a new dependency — just call one method.&lt;/p&gt;
&lt;h3&gt;Installation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install selenium webdriver-manager&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;webdriver-manager&lt;/code&gt; package automatically downloads the right Chrome driver. Without it, you'd have to manually download chromedriver and keep its version in sync with your browser version. If you've ever tried doing that by hand, you know it's an adventure on its own.&lt;/p&gt;
&lt;h3&gt;Basic screenshot&lt;/h3&gt;
&lt;p&gt;Create a file called &lt;code&gt;selenium_screenshot.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver


&lt;p&gt;from selenium.webdriver.chrome.options import Options&lt;br&gt;
from selenium.webdriver.chrome.service import Service&lt;br&gt;
from webdriver_manager.chrome import ChromeDriverManager&lt;/p&gt;

&lt;h1&gt;
  
  
  Set up headless mode so the browser doesn't open on screen
&lt;/h1&gt;

&lt;p&gt;options = Options()&lt;br&gt;
options.add_argument('--headless=new')&lt;br&gt;
options.add_argument('--window-size=1280,800')&lt;/p&gt;

&lt;h1&gt;
  
  
  Create a driver with automatic chromedriver installation
&lt;/h1&gt;

&lt;p&gt;driver = webdriver.Chrome(&lt;br&gt;
    service=Service(ChromeDriverManager().install()),&lt;br&gt;
    options=options&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Open the page and take a screenshot
&lt;/h1&gt;

&lt;p&gt;driver.get('&lt;a href="https://dev.to'"&gt;https://dev.to&amp;amp;#039;&lt;/a&gt;)&lt;br&gt;
driver.save_screenshot('dev_to.png')&lt;/p&gt;

&lt;p&gt;print(f'Saved: dev_to.png')&lt;br&gt;
driver.quit()&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python selenium_screenshot.py&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FdFBXUKLq0Zx68rnfxyhJqxDPQJtQiizNkCbxMrk3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FdFBXUKLq0Zx68rnfxyhJqxDPQJtQiizNkCbxMrk3.png" width="800" height="688"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The terminal shows &lt;code&gt;Saved: dev_to.png&lt;/code&gt;, and the file appears in the sidebar on the left. Open it up — there's the DEV Community homepage, captured by the headless browser. The image is 1280x661 pixels, about 309 KB. Notice that Selenium only captured the visible part of the page — whatever fits in the browser window. Content below the fold didn't make it into the screenshot.&lt;/p&gt;
&lt;h3&gt;Screenshotting a specific element&lt;/h3&gt;
&lt;p&gt;Sometimes you don't need the whole page — just one particular block. Selenium can capture individual elements too:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver&lt;br&gt;
from selenium.webdriver.chrome.options import Options&lt;br&gt;
from selenium.webdriver.chrome.service import Service&lt;br&gt;
from selenium.webdriver.common.by import By&lt;br&gt;
from webdriver_manager.chrome import ChromeDriverManager

&lt;p&gt;options = Options()&lt;br&gt;
options.add_argument('--headless=new')&lt;br&gt;
options.add_argument('--window-size=1280,800')&lt;/p&gt;

&lt;p&gt;driver = webdriver.Chrome(&lt;br&gt;
    service=Service(ChromeDriverManager().install()),&lt;br&gt;
    options=options&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;driver.get('&lt;a href="https://dev.to'"&gt;https://dev.to&amp;amp;#039;&lt;/a&gt;)&lt;/p&gt;

&lt;h1&gt;
  
  
  Find the element by CSS selector and screenshot just that
&lt;/h1&gt;

&lt;p&gt;header = driver.find_element(By.TAG_NAME, 'header')&lt;br&gt;
header.screenshot('dev_to_header.png')&lt;/p&gt;

&lt;p&gt;print('Saved: header.png')&lt;br&gt;
driver.quit()&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FTivPCW94VDS6X9KSLntGFRqw13WVYOkAcaViUkqg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FTivPCW94VDS6X9KSLntGFRqw13WVYOkAcaViUkqg.png" alt="Selenium element screenshot — only the header of dev.to captured" width="800" height="688"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The result is a narrow strip spanning the full page width. Just the DEV Community navigation bar, nothing else. This comes in handy when you need to capture a signup form, a product card, or any other specific block on the page.&lt;/p&gt;
&lt;h3&gt;Where Selenium falls short&lt;/h3&gt;
&lt;p&gt;Selenium has one notable limitation — it can't do full-page screenshots out of the box. The &lt;code&gt;save_screenshot()&lt;/code&gt; method only captures the viewport. There are workarounds involving JavaScript scrolling and image stitching, but they're tedious and unreliable. If you need a full-page screenshot, that's where Playwright comes in.&lt;/p&gt;
&lt;h2&gt;Method 2: Playwright&lt;/h2&gt;
&lt;p&gt;Playwright is a more modern alternative from Microsoft. It supports Chrome, Firefox, and Safari through a single API. For screenshots, it beats Selenium for one simple reason — it can do full-page captures by adding literally one line of code.&lt;/p&gt;
&lt;h3&gt;Installation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install playwright&lt;br&gt;
playwright install chromium&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second command downloads Chromium. If you need Firefox or WebKit, you can specify &lt;code&gt;playwright install firefox webkit&lt;/code&gt;. For screenshots, Chromium is more than enough.&lt;/p&gt;
&lt;h3&gt;Basic screenshot&lt;/h3&gt;
&lt;p&gt;File &lt;code&gt;playwright_screenshot.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from playwright.sync_api import sync_playwright

&lt;p&gt;with sync_playwright() as p:&lt;br&gt;
    browser = p.chromium.launch()&lt;br&gt;
    page = browser.new_page(viewport={'width': 1280, 'height': 800})&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;page.goto(&amp;amp;#039;https://news.ycombinator.com&amp;amp;#039;)
page.screenshot(path=&amp;amp;#039;hackernews.png&amp;amp;#039;)

print(&amp;amp;#039;Saved: hackernews.png&amp;amp;#039;)
browser.close()&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;p&amp;gt;Run it:&amp;lt;/p&amp;gt;&amp;lt;pre&amp;gt;&amp;lt;code class="language-bash"&amp;gt;python playwright_screenshot.py&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;p&amp;gt;&amp;lt;img src="https://screenshotrun.com/storage/blog-attachments/0z2WFKyi1rfotmXgs43tuzbb0PXJT6IlKnipeOxg.png" alt="Playwright screenshot of Hacker News — code in editor, result in VS Code preview, terminal output" data-id="blog-attachments/0z2WFKyi1rfotmXgs43tuzbb0PXJT6IlKnipeOxg.png"&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;You can see the whole process in the terminal: Playwright installed first, then the script ran and printed &amp;lt;code&amp;gt;Saved: hackernews.png&amp;lt;/code&amp;gt;. The preview shows a screenshot of Hacker News — orange header, list of posts, everything just like in a browser. The code is noticeably more compact than Selenium — no fiddling with drivers and options. But the real advantage of Playwright is coming up next.&amp;lt;/p&amp;gt;&amp;lt;h3&amp;gt;Full-page screenshot&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;This is what makes Playwright worth picking. One option, &amp;lt;code&amp;gt;full_page=True&amp;lt;/code&amp;gt;, and you get a screenshot of the entire page from top to bottom:&amp;lt;/p&amp;gt;&amp;lt;pre&amp;gt;&amp;lt;code class="language-python"&amp;gt;from playwright.sync_api import sync_playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;with sync_playwright() as p:&lt;br&gt;
    browser = p.chromium.launch()&lt;br&gt;
    page = browser.new_page(viewport={'width': 1280, 'height': 800})&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;page.goto(&amp;amp;#039;https://news.ycombinator.com&amp;amp;#039;)
page.screenshot(path=&amp;amp;#039;hackernews_full.png&amp;amp;#039;, full_page=True)

print(&amp;amp;#039;Full-page screenshot saved: hackernews_full.png&amp;amp;#039;)
browser.close()&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;p&amp;gt;&amp;lt;img src="https://screenshotrun.com/storage/blog-attachments/3jIEfY6kylRUeWj1dZ2MCl6y1CkCtbAv9WfIbABs.png" alt="Playwright full-page screenshot of Hacker News — complete page from top to bottom" data-id="blog-attachments/3jIEfY6kylRUeWj1dZ2MCl6y1CkCtbAv9WfIbABs.png"&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;The difference is obvious. A regular screenshot gets cut off at the viewport height, while the full-page version captures everything — all 30 posts plus the footer with the search bar. Try doing that in Selenium with a single line — you can&amp;amp;#039;t.&amp;lt;/p&amp;gt;&amp;lt;h3&amp;gt;Mobile viewport&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;Let&amp;amp;#039;s say you need to see how a site looks on a phone. Playwright lets you emulate specific devices:&amp;lt;/p&amp;gt;&amp;lt;pre&amp;gt;&amp;lt;code class="language-python"&amp;gt;from playwright.sync_api import sync_playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;with sync_playwright() as p:&lt;br&gt;
    browser = p.chromium.launch()&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Emulate iPhone 14
iphone = p.devices[&amp;amp;#039;iPhone 14&amp;amp;#039;]
context = browser.new_context(**iphone)
page = context.new_page()

page.goto(&amp;amp;#039;https://news.ycombinator.com&amp;amp;#039;)
page.screenshot(path=&amp;amp;#039;hackernews_mobile.png&amp;amp;#039;)

print(&amp;amp;#039;Mobile screenshot saved: hackernews_mobile.png&amp;amp;#039;)
browser.close()&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;p&amp;gt;&amp;lt;img src="https://screenshotrun.com/storage/blog-attachments/p0m0VdCcCbqwezeVtoQda6NbDExiKsTte0RTAwoa.png" alt="Playwright mobile screenshot — Hacker News rendered as iPhone 14 viewport" data-id="blog-attachments/p0m0VdCcCbqwezeVtoQda6NbDExiKsTte0RTAwoa.png"&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;The screenshot shows Hacker News fully adapted to a mobile screen — posts stack in a single column, fonts are larger, navigation is rearranged. Full mobile emulation. Playwright knows the specs of dozens of devices: iPhone, Pixel, iPad, and others. No need to manually figure out viewport widths and user agents.&amp;lt;/p&amp;gt;&amp;lt;h3&amp;gt;Waiting for content to load&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;Another common problem — the screenshot fires before the page has finished loading. This is especially painful with JavaScript-heavy pages. Playwright can wait for a specific element to appear:&amp;lt;/p&amp;gt;&amp;lt;pre&amp;gt;&amp;lt;code class="language-python"&amp;gt;from playwright.sync_api import sync_playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;with sync_playwright() as p:&lt;br&gt;
    browser = p.chromium.launch()&lt;br&gt;
    page = browser.new_page(viewport={'width': 1280, 'height': 800})&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;page.goto(&amp;amp;#039;https://github.com/trending&amp;amp;#039;)
# Wait for the repository list to appear
page.wait_for_selector(&amp;amp;#039;article.Box-row&amp;amp;#039;)
page.screenshot(path=&amp;amp;#039;github_trending.png&amp;amp;#039;)

print(&amp;amp;#039;Screenshot GitHub Trending saved: github_trending.png&amp;amp;#039;)
browser.close()&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;p&amp;gt;&amp;lt;img src="https://screenshotrun.com/storage/blog-attachments/ZxpIF5MHmKqddDNVIhBVLFCS2FCuWIytxQao9wXJ.png" alt="Playwright screenshot of GitHub Trending page with wait_for_selector — repositories fully loaded" data-id="blog-attachments/ZxpIF5MHmKqddDNVIhBVLFCS2FCuWIytxQao9wXJ.png"&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;The screenshot shows the GitHub Trending page with the repository list fully loaded. You can see stars, forks, descriptions — all the content is there. Without &amp;lt;code&amp;gt;wait_for_selector&amp;lt;/code&amp;gt; you might have gotten a half-empty page with just a spinner. With it, Playwright waits until the target content actually appears in the DOM, then takes the shot.&amp;lt;/p&amp;gt;&amp;lt;h2&amp;gt;Method 3: Screenshot API&amp;lt;/h2&amp;gt;&amp;lt;p&amp;gt;Selenium and Playwright work great on your local machine. But try running them on a server — that&amp;amp;#039;s a different story. Chrome needs to be installed, it pulls in dozens of dependencies, eats RAM, and crashes from time to time. On CI/CD it&amp;amp;#039;s a separate headache: Dockerfiles grow, builds slow down.&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;A screenshot API takes a different approach — you send an HTTP request with a URL, and get a ready screenshot back. No browsers on your side. All the rendering happens remotely.&amp;lt;/p&amp;gt;&amp;lt;h3&amp;gt;How it works&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;I&amp;amp;#039;ll show this using &amp;lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://screenshotrun.com"&amp;gt;ScreenshotRun&amp;lt;/a&amp;gt; — an API I built for exactly this kind of task. The free plan gives you 300 requests per month, which is enough to try it out and see if this approach works for you.&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;First, &amp;lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://screenshotrun.com/register"&amp;gt;sign up&amp;lt;/a&amp;gt; and copy your API key from the dashboard.&amp;lt;/p&amp;gt;&amp;lt;h3&amp;gt;Basic request&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;File &amp;lt;code&amp;gt;api_screenshot.py&amp;lt;/code&amp;gt;:&amp;lt;/p&amp;gt;&amp;lt;pre&amp;gt;&amp;lt;code class="language-python"&amp;gt;import requests
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;API_KEY = 'your-api-key-here'&lt;br&gt;
URL = '&lt;a href="https://producthunt.com'" rel="noopener noreferrer"&gt;https://producthunt.com&amp;amp;#039&lt;/a&gt;;&lt;/p&gt;

&lt;p&gt;response = requests.get(&lt;br&gt;
    '&lt;a href="https://screenshotrun.com/api/v1/screenshots/capture'" rel="noopener noreferrer"&gt;https://screenshotrun.com/api/v1/screenshots/capture&amp;amp;#039&lt;/a&gt;;,&lt;br&gt;
    headers={&lt;br&gt;
        'Authorization': f'Bearer {API_KEY}',&lt;br&gt;
    },&lt;br&gt;
    params={&lt;br&gt;
        'url': URL,&lt;br&gt;
        'format': 'png',&lt;br&gt;
        'width': 1280,&lt;br&gt;
        'height': 800,&lt;br&gt;
        'response_type': 'image',&lt;br&gt;
    },&lt;br&gt;
    timeout=60,&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;if response.status_code == 200:&lt;br&gt;
    with open('producthunt.png', 'wb') as f:&lt;br&gt;
        f.write(response.content)&lt;br&gt;
    print('Screenshot saved: producthunt.png')&lt;br&gt;
else:&lt;br&gt;
    print(f'Error: {response.status_code} — {response.text}')&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install requests&lt;br&gt;
python api_screenshot.py&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No Chromium, no driver. One HTTP request — one file. The &lt;code&gt;requests&lt;/code&gt; library is already installed for most Python developers, so there's nothing extra to set up. Note the &lt;code&gt;response_type: 'image'&lt;/code&gt; parameter — it tells the API to return the binary image directly, without a JSON wrapper. Also worth setting &lt;code&gt;timeout=60&lt;/code&gt;, since rendering heavy pages can take a few seconds.&lt;/p&gt;
&lt;h3&gt;Full-page screenshot via API&lt;/h3&gt;
&lt;p&gt;For a full-page screenshot, add one parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

&lt;p&gt;API_KEY = 'your-api-key-here'&lt;/p&gt;

&lt;p&gt;response = requests.get(&lt;br&gt;
    '&lt;a href="https://screenshotrun.com/api/v1/screenshots/capture'" rel="noopener noreferrer"&gt;https://screenshotrun.com/api/v1/screenshots/capture&amp;amp;#039&lt;/a&gt;;,&lt;br&gt;
    headers={&lt;br&gt;
        'Authorization': f'Bearer {API_KEY}',&lt;br&gt;
    },&lt;br&gt;
    params={&lt;br&gt;
        'url': '&lt;a href="https://github.com/topics'" rel="noopener noreferrer"&gt;https://github.com/topics&amp;amp;#039&lt;/a&gt;;,&lt;br&gt;
        'format': 'png',&lt;br&gt;
        'width': 1280,&lt;br&gt;
        'full_page': True,&lt;br&gt;
        'response_type': 'image',&lt;br&gt;
    },&lt;br&gt;
    timeout=60,&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;if response.status_code == 200:&lt;br&gt;
    with open('github_topics_full.png', 'wb') as f:&lt;br&gt;
        f.write(response.content)&lt;br&gt;
    print('Full-page screenshot saved: github_topics_full.png')&lt;br&gt;
else:&lt;br&gt;
    print(f'Error: {response.status_code}')&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The API scrolls the page itself, waits for lazy-loaded images to appear, and assembles the full snapshot. You don't need to think about scroll logic or timeouts — all of that is handled on the service side.&lt;/p&gt;
&lt;h3&gt;Mobile screenshot via API&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import requests

&lt;p&gt;API_KEY = 'your-api-key-here'&lt;/p&gt;

&lt;p&gt;response = requests.get(&lt;br&gt;
    '&lt;a href="https://screenshotrun.com/api/v1/screenshots/capture'" rel="noopener noreferrer"&gt;https://screenshotrun.com/api/v1/screenshots/capture&amp;amp;#039&lt;/a&gt;;,&lt;br&gt;
    headers={&lt;br&gt;
        'Authorization': f'Bearer {API_KEY}',&lt;br&gt;
    },&lt;br&gt;
    params={&lt;br&gt;
        'url': '&lt;a href="https://stripe.com'" rel="noopener noreferrer"&gt;https://stripe.com&amp;amp;#039&lt;/a&gt;;,&lt;br&gt;
        'format': 'png',&lt;br&gt;
        'width': 390,&lt;br&gt;
        'height': 844,&lt;br&gt;
        'response_type': 'image',&lt;br&gt;
    },&lt;br&gt;
    timeout=60,&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;if response.status_code == 200:&lt;br&gt;
    with open('stripe_mobile.png', 'wb') as f:&lt;br&gt;
        f.write(response.content)&lt;br&gt;
    print('Mobile screenshot saved: stripe_mobile.png')&lt;br&gt;
else:&lt;br&gt;
    print(f'Error: {response.status_code}')&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Width 390 and height 844 match the iPhone 14 screen size. You can emulate any mobile viewport the same way — just change the width and height values.&lt;/p&gt;
&lt;h3&gt;Batch processing multiple URLs&lt;/h3&gt;
&lt;p&gt;This is where the API really shows its strength. Say you need to screenshot several websites in one go:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests&lt;br&gt;
import os

&lt;p&gt;API_KEY = 'your-api-key-here'&lt;/p&gt;

&lt;p&gt;urls = [&lt;br&gt;
    '&lt;a href="https://github.com'" rel="noopener noreferrer"&gt;https://github.com&amp;amp;#039&lt;/a&gt;;,&lt;br&gt;
    '&lt;a href="https://stackoverflow.com'" rel="noopener noreferrer"&gt;https://stackoverflow.com&amp;amp;#039&lt;/a&gt;;,&lt;br&gt;
    '&lt;a href="https://dev.to'"&gt;https://dev.to&amp;amp;#039&lt;/a&gt;;,&lt;br&gt;
    '&lt;a href="https://news.ycombinator.com'" rel="noopener noreferrer"&gt;https://news.ycombinator.com&amp;amp;#039&lt;/a&gt;;,&lt;br&gt;
    '&lt;a href="https://producthunt.com'" rel="noopener noreferrer"&gt;https://producthunt.com&amp;amp;#039&lt;/a&gt;;,&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;os.makedirs('screenshots', exist_ok=True)&lt;/p&gt;

&lt;p&gt;for url in urls:&lt;br&gt;
    domain = url.split('//')[1].split('/')[0].replace('.', '_')&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;response = requests.get(
    &amp;amp;#039;https://screenshotrun.com/api/v1/screenshots/capture&amp;amp;#039;,
    headers={
        &amp;amp;#039;Authorization&amp;amp;#039;: f&amp;amp;#039;Bearer {API_KEY}&amp;amp;#039;,
    },
    params={
        &amp;amp;#039;url&amp;amp;#039;: url,
        &amp;amp;#039;format&amp;amp;#039;: &amp;amp;#039;png&amp;amp;#039;,
        &amp;amp;#039;width&amp;amp;#039;: 1280,
        &amp;amp;#039;height&amp;amp;#039;: 800,
        &amp;amp;#039;response_type&amp;amp;#039;: &amp;amp;#039;image&amp;amp;#039;,
    },
    timeout=60,
)

if response.status_code == 200:
    filepath = f&amp;amp;#039;screenshots/{domain}.png&amp;amp;#039;
    with open(filepath, &amp;amp;#039;wb&amp;amp;#039;) as f:
        f.write(response.content)
    print(f&amp;amp;#039;✓ {domain}&amp;amp;#039;)
else:
    print(f&amp;amp;#039;✗ {domain}: {response.status_code}&amp;amp;#039;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;print(f'\nReady screenshots/')&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FeEaR1RNtuVemthkCxg5HjYgrLftdVR12Rh0aEluf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fscreenshotrun.com%2Fstorage%2Fblog-attachments%2FeEaR1RNtuVemthkCxg5HjYgrLftdVR12Rh0aEluf.png" alt="Batch screenshot results — screenshots folder with multiple PNG files, terminal showing progress" width="800" height="681"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Checkmarks roll through the terminal: &lt;code&gt;✓ github_com&lt;/code&gt;, &lt;code&gt;✓ stackoverflow_com&lt;/code&gt;, &lt;code&gt;✓ dev_to&lt;/code&gt;... PNG files appear in the &lt;code&gt;screenshots/&lt;/code&gt; folder, one per site. You might notice one of the requests returned a 429 (rate limit). The free plan has a limit on concurrent requests. For production use, you can either upgrade to a paid plan or add a pause between requests with &lt;code&gt;time.sleep(1)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Five sites, five files, 35 lines of code. With Selenium you'd have to launch and shut down the browser for each URL. With the API, each request is just a regular HTTP call — no different from calling any other REST API.&lt;/p&gt;
&lt;h2&gt;Selenium vs Playwright vs API — an honest comparison&lt;/h2&gt;
&lt;p&gt;I've tried all three approaches. Here's what I think.&lt;/p&gt;
&lt;h3&gt;Selenium&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;When it fits:&lt;/strong&gt; you already have Selenium in your project for testing or scraping. Adding &lt;code&gt;save_screenshot()&lt;/code&gt; to existing code takes one line. Installing Selenium just for screenshots isn't worth it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; the most mature tool out there, with a huge amount of documentation and examples online. Supports all major browsers: Chrome, Firefox, Safari, Edge. You can interact with the page before taking a screenshot — click buttons, fill forms, scroll. Works well with pytest for testing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; no full-page screenshots out of the box, only viewport. You need to keep chromedriver version in sync with the browser (webdriver-manager helps, but doesn't always solve this). Verbose code even for simple tasks. On a server without a GUI, you need to configure headless mode and install rendering dependencies.&lt;/p&gt;
&lt;h3&gt;Playwright&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;When it fits:&lt;/strong&gt; you're writing a script from scratch and need full-page screenshots, mobile emulation, or smart waiting. For local work and CI/CD with moderate volume, it's the best choice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; full-page screenshots with a single line — &lt;code&gt;full_page=True&lt;/code&gt;. Built-in mobile device emulation with a ready-made device database. Smart waiting — you can wait for a specific element, network idle, or a particular page state. Clean, compact API with less boilerplate compared to Selenium. Same API for Chromium, Firefox, and WebKit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Chromium weighs about 400 MB and downloads on first install. Each running browser instance consumes 200-400 MB of RAM. On servers you still need system dependencies for rendering (fonts, libraries). For 100+ screenshots you need to think about parallelism and browser pools.&lt;/p&gt;
&lt;h3&gt;Screenshot API&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;When it fits:&lt;/strong&gt; production, automation, server-side scripts. When you don't want to deal with browser installation and resource management. When you need stability and predictability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; zero dependencies on your side — just &lt;code&gt;requests&lt;/code&gt;. Works on any platform that has an HTTP client: a server, a Lambda function, a Jupyter Notebook. Doesn't consume your server's RAM or CPU. Easy to scale — no difference between 1 screenshot and 1000. The API handles cookie banners, lazy loading, and JavaScript rendering on its own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; you depend on an external service — if the API is down, no screenshots. The free plan has limits (300 requests/month on ScreenshotRun). A small network delay — each request takes 3-10 seconds instead of 1-3 seconds locally. Paid plans for higher volumes start at $9/month. You can't interact with the page (click, log in) — it's screenshot by URL only.&lt;/p&gt;
&lt;h3&gt;Bottom line&lt;/h3&gt;
&lt;p&gt;For one-off screenshots on your own machine — &lt;strong&gt;Playwright&lt;/strong&gt;. For integrating into an existing test infrastructure — &lt;strong&gt;Selenium&lt;/strong&gt;. For server automation and batch processing — &lt;strong&gt;Screenshot API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://screenshotrun.com"&gt;ScreenshotRun&lt;/a&gt; gives you 300 free requests per month — try it and decide for yourself.&lt;/p&gt;
&lt;h2&gt;Tips that will save you time&lt;/h2&gt;
&lt;p&gt;A few things I figured out in practice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Waiting for page load.&lt;/strong&gt; Pages with heavy JavaScript might not finish rendering by the time the screenshot fires. In Playwright, use &lt;code&gt;wait_for_selector()&lt;/code&gt; to wait for a specific element, or &lt;code&gt;wait_for_load_state('networkidle')&lt;/code&gt; to wait until all network requests have settled. With the API approach, this is handled on the service side — it waits for full load automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cookie banners and popups.&lt;/strong&gt; They constantly cover content in screenshots. In Selenium and Playwright you can dismiss them via JavaScript injection: find the "Accept" button or the entire banner and remove it from the DOM. The ScreenshotRun API can block cookie banners automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Server resources.&lt;/strong&gt; Each Chrome instance consumes 200-400 MB of RAM. Ten parallel browsers — that's already 2-4 GB just for screenshots. By comparison, the API approach uses approximately 0 MB of your RAM, because rendering happens on someone else's server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File format.&lt;/strong&gt; PNG is for pixel-perfect images with no quality loss. JPEG is for when file size matters and you can sacrifice some detail. WebP strikes a good balance between size and quality, though not all image viewers support it yet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rate limits.&lt;/strong&gt; If you're making batch requests through an API, add &lt;code&gt;time.sleep(1)&lt;/code&gt; between calls. Most API services limit the number of requests per second, and without a pause some requests will return a 429 error.&lt;/p&gt;
&lt;p&gt;So we've covered all three approaches to taking website screenshots with Python — from local libraries with full control to a third-party API with zero setup. I've laid out the pros and cons, provided code examples. All that's left is to try them and pick what works best for your use case.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>saas</category>
    </item>
    <item>
      <title>5 Ways Developers Use Screenshot APIs (Beyond Simple Page Captures)</title>
      <dc:creator>Vitalii Holben</dc:creator>
      <pubDate>Fri, 27 Mar 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/webmox/5-ways-developers-use-screenshot-apis-beyond-simple-page-captures-1ep2</link>
      <guid>https://dev.to/webmox/5-ways-developers-use-screenshot-apis-beyond-simple-page-captures-1ep2</guid>
      <description>&lt;p&gt;When people hear "screenshot API," most of them picture a pretty straightforward task: send a URL, get back an image of the page. And that is the basic scenario, sure. But developers who've already integrated a screenshot API into their projects tend to use it for things you might not have considered at all.&lt;/p&gt;

&lt;p&gt;In this article, I've put together five real scenarios where a screenshot API can save you hours of work and solve problems that are either difficult or expensive to handle any other way.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Automatic OG Image Generation
&lt;/h2&gt;

&lt;p&gt;Open Graph images are those previews that pop up when you share a link on Slack, Twitter, LinkedIn, or WhatsApp. If a page doesn't have an OG image, the shared link looks bland and gets fewer clicks. I actually covered how to create Open Graph images both manually and automatically in a previous article, so feel free to check that out for more detail.&lt;/p&gt;

&lt;p&gt;As I mentioned there, the core problem is that every page needs its own image. If you have a blog with 50 posts, that's 50 images you need to create and maintain. And if you're running a SaaS product with dynamic pages — user profiles, dashboards, reports — then creating OG images by hand isn't realistic at all.&lt;/p&gt;

&lt;p&gt;In cases like these, a screenshot API makes the whole thing much simpler, and the workflow is very sequential: you build an HTML template with the design you want (title, logo, background), render it through the API, and get back a ready-made image at 1200×630 — exactly the size social networks recommend. The entire process is automatic, with images generated on the fly or on a schedule. I won't go into too much detail here since I covered the full breakdown in that earlier post.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Link Previews for Directories and Catalogs
&lt;/h2&gt;

&lt;p&gt;If you're building a site directory, a marketplace, an aggregator, or really any product where users submit links to external resources, you need visual previews of those links. Without them, your catalog ends up looking like a boring list of URLs.&lt;/p&gt;

&lt;p&gt;A screenshot API lets you automatically generate a thumbnail for every site that gets added. In practice, it works like this: a user pastes a URL, your backend fires off a request to the API, gets back an image, and displays it as a card. No manual work involved, and no need to ask users to upload screenshots on their own.&lt;/p&gt;

&lt;p&gt;This approach is used by site directories, aggregators like Product Hunt, bookmarking tools, and even internal corporate portals where employees share useful resources. Now, you might say that AI image generators can quickly produce any image you need these days, but this is a different thing entirely. With a screenshot API, you send dynamic HTML and get back a preview image in the exact style and design you prepared — no hallucinations, no randomness, just precisely what you asked for. On top of that, it's significantly faster than working with an AI model to generate images one by one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Visual Regression Testing During Deploys
&lt;/h2&gt;

&lt;p&gt;This is probably one of the most practical scenarios for developers. The idea is simple: before each deploy, you take screenshots of your key pages, then take them again after the deploy and compare. If something broke visually, you catch it before your users do.&lt;/p&gt;

&lt;p&gt;Without a screenshot API, this process requires running a headless browser on your CI/CD server, which adds a fair amount of complexity: you need to install Chromium, manage memory, handle timeouts. I actually wrote about this whole process in detail in my article on how to take a website screenshot with Node.js. A screenshot API strips away all that infrastructure — you just send an HTTP request and get back an image.&lt;/p&gt;

&lt;p&gt;A typical pipeline looks something like this: your CI/CD triggers a script that captures screenshots of 10 to 20 key pages through the API, compares them pixel by pixel against the previous versions, and sends an alert to Slack if the difference exceeds a set threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Archiving and Compliance
&lt;/h2&gt;

&lt;p&gt;In certain industries — finance, legal, pharma — companies are required to keep a record of what their website looked like at a given point in time. This comes up during audits, legal disputes, and regulatory compliance checks.&lt;/p&gt;

&lt;p&gt;A screenshot API lets you automatically take daily or weekly snapshots of the pages you need and save them with a timestamp. And then if a dispute ever comes up, you have visual proof of exactly what was published on the site on a specific date.&lt;/p&gt;

&lt;p&gt;This same approach is also useful for marketing teams that deal with sponsored content. Advertisers regularly ask for proof that their placement went live, and an automated screenshot with a date and URL handles that without any manual effort on anyone's part.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Competitor Monitoring and Change Tracking
&lt;/h2&gt;

&lt;p&gt;The last scenario is taking regular screenshots of competitor pages to track changes. This could be a pricing page, a homepage, an ad campaign landing page, or even search results for specific keywords.&lt;/p&gt;

&lt;p&gt;Developers typically set up a cron job that once a day (or more often) captures screenshots of a list of URLs through the API and stores them. Over time, this builds up a visual history of changes that you can analyze — when a competitor updated their pricing, redesigned their site, or launched a new campaign.&lt;/p&gt;

&lt;p&gt;Some go even further and add automated screenshot comparison: if the current snapshot differs from the previous one by more than a certain percentage, the system fires off a notification. That way, you find out about changes on competitor sites the same day, without having to check them manually. I'm actually planning to add this feature to our service in the future as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Common Thread
&lt;/h2&gt;

&lt;p&gt;If you look at all five scenarios, there's one idea running through them: a screenshot API turns visual information into data you can work with programmatically. Instead of opening a browser, manually taking a screenshot, and saving it somewhere, you send an HTTP request and get back an image that you can process, store, compare, or display to a user — all of it automatically.&lt;/p&gt;

&lt;p&gt;And the more pages you need to handle, the more time it saves.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://screenshotrun.com" rel="noopener noreferrer"&gt;Screenshotrun&lt;/a&gt; API lets you capture screenshots of any web page with a single HTTP request. 200 free screenshots per month, Bearer authentication, full-page and viewport modes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>saas</category>
      <category>productivity</category>
    </item>
    <item>
      <title>What Competitor Pages to Monitor and How Often to Take Screenshots</title>
      <dc:creator>Vitalii Holben</dc:creator>
      <pubDate>Thu, 26 Mar 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/webmox/what-competitor-pages-to-monitor-and-how-often-to-take-screenshots-12l0</link>
      <guid>https://dev.to/webmox/what-competitor-pages-to-monitor-and-how-often-to-take-screenshots-12l0</guid>
      <description>&lt;p&gt;Most companies understand that keeping an eye on competitors is important. But in practice, it usually goes something like this: once a month, someone opens a competitor's website, scrolls through the homepage, glances at a few other pages, decides nothing has really changed, and closes the tab. Then we're caught off guard when that same competitor launches a new feature, changes their pricing, or reworks their positioning entirely — and we find out about it last.&lt;/p&gt;

&lt;p&gt;The problem isn't that people don't want to monitor competitors. More often, the real issue is simpler — it's unclear what exactly to monitor and how frequently. Tracking every single page is too time-consuming, if not impossible. But ignoring competitors altogether leads to exactly the kind of surprises we just described: a new feature appears on their site, and you had no idea it was coming. In this article, we'll go over the specific pages worth monitoring, recommend a checking frequency for each type, and look at how to automate the entire process so it runs with minimal effort on your part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With Five Competitors, No More
&lt;/h2&gt;

&lt;p&gt;The first mistake is trying to monitor everyone. You might have 20 competitors in your space, but only 3 to 5 of them actually affect your business. These are the companies targeting the same audience, operating in the same price range, and showing up in the same search results — the ones who are genuinely your direct competitors, not just other players in the market.&lt;/p&gt;

&lt;p&gt;We recommend picking five primary or potential competitors. For each one, identify 5 to 10 key URLs on their site. That gives you roughly 25 to 50 pages total, which is a perfectly manageable volume for automated page monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pricing Page Is the Most Valuable One
&lt;/h2&gt;

&lt;p&gt;If you can only monitor a single page on a competitor's site, make it the pricing page. Price changes are a direct signal of strategic decisions. If a competitor raises their prices, it usually means they feel confident about growth and aren't worried about losing customers over a price bump. On the other hand, if they drop prices, the picture is different — they may be fighting for market share or struggling with churn.&lt;/p&gt;

&lt;p&gt;But it's not just about the numbers themselves. Pay attention to a few other things as well:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan structure.&lt;/strong&gt; If they had three plans and now there are four, that's already worth thinking about. If they added an enterprise tier with a "contact us" label instead of a price, it signals a move toward larger customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature gating.&lt;/strong&gt; Which features ended up in the free plan and which ones are behind a paywall? When features get moved between plans, it's usually a strong signal of where their priorities are shifting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Presentation.&lt;/strong&gt; Did they switch to "Starting at $9/mo" instead of showing exact figures? Did they make annual billing the default option? Every detail like this is typically the result of an A/B test or a deliberate strategic decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often to monitor:&lt;/strong&gt; We recommend about once a week. Prices don't change every day, but missing a change for two weeks means losing time and failing to draw the right conclusions for your own strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Homepage Is a Mirror of Positioning
&lt;/h2&gt;

&lt;p&gt;The homepage is how a company wants to be perceived. Every change on it is a deliberate choice. A new headline means they've shifted their messaging. If they removed a section with client logos and replaced it with case studies, they're switching from social proof to proof of value.&lt;/p&gt;

&lt;p&gt;Here's what to watch for on the homepage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hero section, or the very first screen of the site:&lt;/strong&gt; the headline, subheadline, and CTA. When a competitor changes their main headline, it's always meaningful. It means someone was given a task and spent time finding a better way to phrase what the company does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Social proof:&lt;/strong&gt; client logos, numbers ("10,000+ teams"), testimonials. If a large company's logo appears on their site for the first time, it's a clear sign the competitor just closed a major deal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigation:&lt;/strong&gt; new menu items often signal the launch of new products or sections and point to the company expanding its overall concept.&lt;/p&gt;

&lt;p&gt;As we mentioned above, the homepage should be monitored about once a week. It doesn't get updated very frequently, but when it does, the changes are almost always significant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Features Page and Changelog
&lt;/h2&gt;

&lt;p&gt;If a competitor maintains a public changelog or a "What's New" page, it's a goldmine. You can literally see what their development team is working on and which features they consider important enough to announce.&lt;/p&gt;

&lt;p&gt;The features page (/features, /product) is also useful but changes less often. When it does change, though, it's typically a major update — new integrations, new capabilities, or a complete rethink of the product lineup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often:&lt;/strong&gt; the changelog should be checked about twice a week. The features page is fine at once every two weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Blog and Company News Feed
&lt;/h2&gt;

&lt;p&gt;A competitor's blog or news feed reveals two things: what topics they're betting on (their SEO strategy) and how they talk to their audience (tone of voice, the pain points they're highlighting).&lt;/p&gt;

&lt;p&gt;You don't need to monitor every individual post. It's enough to track the main blog page — /blog — and see what new posts appear, how often they're publishing, and what topics they cover.&lt;/p&gt;

&lt;p&gt;If a competitor suddenly starts publishing three articles a week instead of one, it means they've hired a content team, they're investing in SEO, or it could even signal a traffic drop they're trying to compensate for. If their topics shift from how-to guides to case studies, they're moving from attracting traffic to converting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Career Pages
&lt;/h2&gt;

&lt;p&gt;The next page that's definitely worth tracking at least twice a month is the careers page, because job listings are an underrated source of competitive intelligence. If a competitor is hiring five ML engineers, that tells a very different story than if they're looking for three enterprise sales reps. Open positions reveal strategic direction earlier than any press release.&lt;/p&gt;

&lt;p&gt;Monitor /careers or /jobs. Screenshots are especially useful here because job listings appear and disappear, and without an archive you won't notice the pattern.&lt;/p&gt;

&lt;p&gt;Why once every two weeks? Because job postings don't update that frequently, and a two-week interval is sufficient to track hiring trends over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Landing Pages for Specific Campaigns
&lt;/h2&gt;

&lt;p&gt;If you know a competitor is running ad campaigns, it's worth monitoring their landing pages. These pages show which messages and offers are working (or being tested) to attract customers.&lt;/p&gt;

&lt;p&gt;The tricky part is that landing pages often live on subdomains or at unpredictable URLs. But if you've found them, add them to your monitoring list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often:&lt;/strong&gt; daily, as long as the campaign is active. Landing pages change frequently — A/B tests, different variants, updated offers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page Type&lt;/th&gt;
&lt;th&gt;Example URL&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pricing&lt;/td&gt;
&lt;td&gt;/pricing&lt;/td&gt;
&lt;td&gt;Once a week&lt;/td&gt;
&lt;td&gt;Direct signal of strategy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Homepage&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;Once a week&lt;/td&gt;
&lt;td&gt;Positioning and messaging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changelog&lt;/td&gt;
&lt;td&gt;/changelog, /whats-new&lt;/td&gt;
&lt;td&gt;Twice a week&lt;/td&gt;
&lt;td&gt;Product development pace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Features&lt;/td&gt;
&lt;td&gt;/features, /product&lt;/td&gt;
&lt;td&gt;Every 2 weeks&lt;/td&gt;
&lt;td&gt;Major updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blog&lt;/td&gt;
&lt;td&gt;/blog&lt;/td&gt;
&lt;td&gt;Once a week&lt;/td&gt;
&lt;td&gt;SEO and content strategy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Careers&lt;/td&gt;
&lt;td&gt;/careers, /jobs&lt;/td&gt;
&lt;td&gt;Every 2 weeks&lt;/td&gt;
&lt;td&gt;Strategic direction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Landing pages&lt;/td&gt;
&lt;td&gt;Various&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;Marketing campaigns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For 5 competitors with 7 to 8 pages each, that's roughly 35 to 40 URLs. At an average frequency of once per week, you're looking at about 150 to 200 screenshots per month. That's a perfectly manageable volume for an automated tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual Monitoring Doesn't Work
&lt;/h2&gt;

&lt;p&gt;You could try doing all of this by hand. Open each page, take a screenshot, save it in a folder with today's date. That approach works for a week, maybe two. Then you forget, skip a day, lose the thread.&lt;/p&gt;

&lt;p&gt;The problem with manual monitoring isn't laziness — it's scale. Checking 40 URLs once a week means 40 tabs, 40 screenshots, 40 files to organize. And that's assuming you only have 5 competitors. On top of that, you still need to compare current screenshots with previous ones to actually spot the changes.&lt;/p&gt;

&lt;p&gt;An automated screenshot tool solves all of these problems. You set up your URLs and schedule once, then simply review the results whenever it's convenient. And if the tool also highlights visual differences between screenshots, you can immediately see what changed without having to compare images side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do With the Data You Collect
&lt;/h2&gt;

&lt;p&gt;Screenshots on their own are just images. The real value comes when you start analyzing patterns:&lt;/p&gt;

&lt;p&gt;If a competitor changed their pricing page three times in a single quarter, they're likely testing their monetization model — probably because the current one isn't performing well.&lt;/p&gt;

&lt;p&gt;If their homepage shifted from "for developers" to "for teams," the competitor is moving upmarket.&lt;/p&gt;

&lt;p&gt;If the changelog gets updated once a week consistently, you're probably looking at a well-funded team with a regular release cycle.&lt;/p&gt;

&lt;p&gt;If positions like "Head of Partnerships" and "Enterprise Account Executive" show up on their careers page, the competitor is gearing up for an enterprise push.&lt;/p&gt;

&lt;p&gt;Once a month, it's worth spending 30 minutes going through your screenshot archive and noting the trends. It won't replace a deep competitive analysis, but it gives you a steady stream of signals that help you make decisions faster.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://snapshotarchive.com" rel="noopener noreferrer"&gt;Snapshot Archive&lt;/a&gt; automatically captures screenshots of any web page on a schedule and highlights visual differences between snapshots. Set up competitor monitoring in 5 minutes — no code, no manual work.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>startup</category>
      <category>saas</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Take a Website Screenshot with Node.js (Playwright, Puppeteer, API)</title>
      <dc:creator>Vitalii Holben</dc:creator>
      <pubDate>Thu, 26 Mar 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/webmox/how-to-take-a-website-screenshot-with-nodejs-playwright-puppeteer-api-4goi</link>
      <guid>https://dev.to/webmox/how-to-take-a-website-screenshot-with-nodejs-playwright-puppeteer-api-4goi</guid>
      <description>&lt;p&gt;Today I'll show you how to take a screenshot of a web page using Node.js. We'll go through actual implementation, look at what can go wrong, and figure out how to deal with it.&lt;/p&gt;

&lt;p&gt;In this guide we'll cover three working approaches: Playwright, Puppeteer, and a screenshot API. We'll start with the simplest script and then step by step work our way up to full-page captures, custom viewport sizes, mobile emulation, and even try to get around cookie banners and lazy-loaded page elements. Fair warning: this guide is long, but it covers a lot of ground and you'll pick up some things you probably haven't seen before.&lt;/p&gt;

&lt;p&gt;To keep things clear, we'll run scripts step by step, add features one at a time, and look at the output after each change. Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before we start
&lt;/h2&gt;

&lt;p&gt;You need Node.js 18 or newer. Let's check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see v18 or higher, you're good. Otherwise grab the latest LTS from &lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;nodejs.org&lt;/a&gt;. Here's what I've got at the moment:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvd5gdgifbyyd467rqii6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvd5gdgifbyyd467rqii6.png" alt="Terminal showing node -v output: v24.3.0" width="800" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Works for us. Now create the project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;screenshots
&lt;span class="nb"&gt;cd &lt;/span&gt;screenshots
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fckmjc5nwmob3tv2bimoo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fckmjc5nwmob3tv2bimoo.png" alt="Terminal showing mkdir, cd, npm init -y with package.json output" width="800" height="701"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Folder created, we're inside it. Let's move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Playwright
&lt;/h2&gt;

&lt;p&gt;Playwright is my go-to tool for browser automation. It's maintained by Microsoft, supports Chromium, Firefox, and WebKit out of the box, and the API is clean. Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This downloads Chromium, Firefox, and WebKit binaries. About 400-500 MB total. If you only need Chromium, you can save time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Basic screenshot
&lt;/h3&gt;

&lt;p&gt;Create a file called &lt;code&gt;screenshot.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ready. github.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqultv75euamgats9lvgl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqultv75euamgats9lvgl.png" alt="VS Code showing screenshot.js with Playwright code" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node screenshot.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of seconds and it's done. An image file appears in the project directory. Let's open it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnmy72r2m4j9gw6icl8w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnmy72r2m4j9gw6icl8w.png" alt="Screenshot of GitHub homepage captured by Playwright at 1280x720" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We've got our first result: an image at 1280x720 pixels. That's Playwright's default viewport. The screenshot captured only what fits in this viewport, roughly the top part of the page. Where 720 pixels ended, the image got cut off.&lt;/p&gt;

&lt;p&gt;Obviously that's not quite what we want, so let's keep going.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full-page screenshot
&lt;/h3&gt;

&lt;p&gt;Usually you need the entire page, not just what fits in the window. Let's fix that. We only need to change one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-full.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fullPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ready. github-full.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it again with &lt;code&gt;node screenshot.js&lt;/code&gt; and open the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdnhlthwe739d4p22641.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdnhlthwe739d4p22641.png" alt="Full-page screenshot of GitHub.com from header to footer" width="800" height="1152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The image is now much taller, from the header all the way down to the footer. Width stays the same (1280px), height is whatever the page needed.&lt;/p&gt;

&lt;p&gt;One thing to keep in mind: Chromium has a hard limit on screenshot height of 16,384 pixels. If the page is longer than that, the content will start repeating. This is a browser limitation, not a Playwright bug. For very long pages you'll need to capture them in chunks. But we're getting decent results already. Let's keep going.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom viewport size
&lt;/h3&gt;

&lt;p&gt;1280x720 might not work for you. Say you need a screenshot that looks like a 1920x1080 desktop monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-1080p.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the script and open the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcm3bl247f7w2zqyj8en6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcm3bl247f7w2zqyj8en6.png" alt="Screenshot of GitHub.com at 1920x1080 viewport" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The page now renders at 1920px width. If the site has a max-width container of 1200px, you'll see whitespace on the sides, exactly like on a real monitor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile screenshot
&lt;/h3&gt;

&lt;p&gt;Playwright has built-in device profiles. Each one sets the viewport, user agent, pixel density, and touch support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iPhone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iPhone 14&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;iPhone&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-mobile.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgc2a1xw8kgmtotactnse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgc2a1xw8kgmtotactnse.png" alt="Mobile screenshot of GitHub.com showing hamburger menu and mobile layout" width="800" height="1362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The page looks like you'd see it on an actual phone: hamburger menu instead of the navigation bar, mobile layout, touch-friendly spacing. The dimensions match the iPhone viewport (390x844 at 3x device pixel ratio).&lt;/p&gt;

&lt;h3&gt;
  
  
  Waiting for content to load
&lt;/h3&gt;

&lt;p&gt;This is where a lot of people get tripped up. You run the script, get a screenshot, and half the page is blank. Images didn't load, dynamic content didn't render, there's a spinner in the middle of the screen.&lt;/p&gt;

&lt;p&gt;The problem is that &lt;code&gt;page.goto()&lt;/code&gt; by default waits for the &lt;code&gt;load&lt;/code&gt; event. It fires when the HTML and its resources are loaded. But it says nothing about JavaScript that runs after load, lazy images, or data fetched from APIs.&lt;/p&gt;

&lt;p&gt;The most reliable option for screenshots is &lt;code&gt;networkidle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this setting, it waits until there are no network requests for 500ms. Yes, it slows things down, but you get practically all the dynamic content in the output.&lt;/p&gt;

&lt;p&gt;It gets even better though. If you know exactly what you're waiting for, you can specify it in the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wait for a specific element to appear&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.main-content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshot.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or just add a delay (not ideal, but sometimes necessary):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// wait 3 seconds&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshot.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Working with different formats: JPEG and PDF
&lt;/h3&gt;

&lt;p&gt;By default screenshots are saved as PNG. But sometimes you need a different format. If file size matters, JPEG can be 3-5x smaller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshot.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or even PDF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth mentioning: PDF only works with Chromium. Firefox and WebKit in Playwright don't support it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Puppeteer
&lt;/h2&gt;

&lt;p&gt;Puppeteer is the other popular option. It's maintained by Google, works with Chrome and Chromium. The API is very similar to Playwright, because Playwright was originally built by the same people who created Puppeteer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;puppeteer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Downloads Chromium (about 170 MB). If you already have Chrome installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;puppeteer-core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you point it to the binary manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic screenshot
&lt;/h3&gt;

&lt;p&gt;Same pattern, let's try making a basic screenshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;puppeteer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-puppeteer.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fee986c9npsukjuijw2b6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fee986c9npsukjuijw2b6.png" alt="Puppeteer screenshot of GitHub.com at default 800x600 viewport" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Puppeteer's default viewport is 800x600. That's narrower than Playwright's 1280x720. This catches some people off guard: the screenshot comes out narrow and the layout might switch to a tablet or even mobile breakpoint.&lt;/p&gt;

&lt;p&gt;Just like in Playwright, you can set the size manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setViewport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-puppeteer.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Full-page screenshot
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-full-puppeteer.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fullPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same &lt;code&gt;fullPage: true&lt;/code&gt; flag. Same 16,384 pixel height limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dealing with cookie banners
&lt;/h3&gt;

&lt;p&gt;These days pretty much every European website shows a GDPR banner, because the law requires it. Neither Playwright nor Puppeteer can block them out of the box, but there are two workarounds:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1:&lt;/strong&gt; Click the banner before taking the screenshot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Try clicking "Accept" if the banner exists&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[class*="cookie"] button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// No banner found, that's fine&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clean.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is pretty unreliable. Every site has its own banner, its own selectors, its own button text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2:&lt;/strong&gt; Hide the banner with CSS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addStyleTag&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    [class*="cookie-banner"],
    [class*="consent"],
    [id*="cookie"],
    .cc-window,
    #onetrust-banner-sdk {
      display: none !important;
    }
  `&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-banner.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trouble with this approach is that you'll keep adding new selectors as you hit new sites. And the banners themselves change over time, so the selectors you're clicking also change. It's a task that sounds simple but turns into endless maintenance at scale. Honestly, there's no clean solution yet beyond specifying which button selector to click to close the banner window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Playwright or Puppeteer: which one to pick?
&lt;/h2&gt;

&lt;p&gt;If you're starting from scratch, I'd go with Playwright.&lt;/p&gt;

&lt;p&gt;Playwright supports three browser engines (Chromium, Firefox, WebKit). Puppeteer only does Chromium. Playwright has smarter auto-waiting, so you spend less time looking for the right screenshot delay to let all dynamic or animated elements load. Device emulation is built into both, and both keep it up to date.&lt;/p&gt;

&lt;p&gt;Puppeteer works fine too. If you've already got a working Puppeteer setup, there's no reason to rewrite it. The screenshot API between the two is nearly identical. But for a new project, Playwright gives you more out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real problems in production
&lt;/h2&gt;

&lt;p&gt;So we know how to take screenshots. At first glance it doesn't seem that hard. But here's what happens when you move from "ran a script on my laptop" to "500 screenshots a day on a server".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory.&lt;/strong&gt; Each Chromium instance eats 200-400 MB of RAM. 10 screenshots in parallel means 2-4 GB just for the browsers. Like any Node.js process, it will eventually crash with OOM, especially if you don't manage instances manually, don't reuse pages, and don't set up a queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeouts.&lt;/strong&gt; Some pages take 15 seconds to load. Some will never finish loading because of an infinite spinner or a broken third-party script. You need timeout handling, retry logic, and realistically you'll never get it fully debugged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fonts.&lt;/strong&gt; Headless Chromium on a Linux server doesn't have the same fonts as your Mac. Screenshots in production will look different. You need to install font packages (at minimum &lt;code&gt;fonts-liberation&lt;/code&gt;, &lt;code&gt;fonts-noto-cjk&lt;/code&gt; for CJK support) or use a Docker image that has them preinstalled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cookie banners, popups, chat widgets.&lt;/strong&gt; We covered this above. Solvable for a single site, but across hundreds of different sites it becomes a job in itself.&lt;/p&gt;

&lt;p&gt;These aren't theoretical problems. I ran into every one of them while building &lt;a href="https://screenshotrun.com" rel="noopener noreferrer"&gt;Screenshotrun&lt;/a&gt;. The screenshot rendering itself is the easy part. Everything around it is where the complexity lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Screenshot API
&lt;/h2&gt;

&lt;p&gt;If you don't want to deal with Chromium instances, fonts, queues, and cookie banner hacks, a screenshot API handles all of that for you. You send an HTTP request with a URL, you get an image back.&lt;/p&gt;

&lt;p&gt;Here's how it looks with &lt;a href="https://screenshotrun.com" rel="noopener noreferrer"&gt;Screenshotrun&lt;/a&gt;. One HTTP call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="s2"&gt;`https://screenshotrun.com/api/v1/screenshots/capture?url=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;response_type=image`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-api.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ready. github-api.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just don't forget to put in your own API key, which you can get for free, along with 300 screenshots per month.&lt;/p&gt;

&lt;p&gt;Run it and open the file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5xzss1wh7jz2iy1gjke.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5xzss1wh7jz2iy1gjke.png" alt="Clean screenshot of GitHub.com captured via Screenshotrun API" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clean screenshot. No Chromium download, no font issues, no memory management. The API handles cookie banners on its own, waits for dynamic content to load, and returns a ready-to-use image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using fetch (Node.js 18+)
&lt;/h3&gt;

&lt;p&gt;If you prefer the &lt;code&gt;fetch&lt;/code&gt; API that ships with Node.js 18:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://screenshotrun.com/api/v1/screenshots/capture?url=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;response_type=image`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-fetch.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ready. github-fetch.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe40hc76y1b5o8x76k7kc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe40hc76y1b5o8x76k7kc.png" alt="Screenshot of GitHub.com captured using fetch with Screenshotrun" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom size and format
&lt;/h3&gt;

&lt;p&gt;Pass parameters in the query string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1920&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;full_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://screenshotrun.com/api/v1/screenshots/capture?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-full.webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcrdwjr39lwuom7h2mslp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcrdwjr39lwuom7h2mslp.png" alt="Full-page WebP screenshot of GitHub.com at 1920px via API" width="800" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You get a full-page WebP screenshot at 1920px wide. The API does all the work: launches a browser, waits for the page to load, scrolls through to trigger lazy images, captures the result, and sends it back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile screenshots via API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;390&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;844&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mobile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://screenshotrun.com/api/v1/screenshots/capture?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-mobile-api.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fizneekmqazbsi892tva3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fizneekmqazbsi892tva3.png" alt="Mobile screenshot of GitHub.com via Screenshotrun API" width="800" height="1192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No need to maintain a device list or configure user agents. Just pass the dimensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use what
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Playwright or Puppeteer&lt;/strong&gt; if you need screenshots occasionally, everything runs locally, and you're willing to maintain the infrastructure. Good for testing, one-off tasks, or when you need full browser control (clicking through forms, filling fields, interacting with the page before capturing).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot API&lt;/strong&gt; if screenshots are part of your product, you need them at scale, or you just don't want to deal with headless browser infrastructure. You trade some flexibility for zero maintenance.&lt;/p&gt;

&lt;p&gt;There's no universal answer. In my own projects I use Playwright in development and testing, and the API in production where reliability and speed matter more than saving a few cents per request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Playwright&lt;/th&gt;
&lt;th&gt;Puppeteer&lt;/th&gt;
&lt;th&gt;Screenshot API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Install size&lt;/td&gt;
&lt;td&gt;~400 MB&lt;/td&gt;
&lt;td&gt;~170 MB&lt;/td&gt;
&lt;td&gt;0 (HTTP request)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browsers&lt;/td&gt;
&lt;td&gt;Chromium, Firefox, WebKit&lt;/td&gt;
&lt;td&gt;Chromium only&lt;/td&gt;
&lt;td&gt;Managed for you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default viewport&lt;/td&gt;
&lt;td&gt;1280x720&lt;/td&gt;
&lt;td&gt;800x600&lt;/td&gt;
&lt;td&gt;1280x720 (configurable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-page capture&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookie banners&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fonts on Linux&lt;/td&gt;
&lt;td&gt;Install yourself&lt;/td&gt;
&lt;td&gt;Install yourself&lt;/td&gt;
&lt;td&gt;Handled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling to 1000+/day&lt;/td&gt;
&lt;td&gt;Your infrastructure&lt;/td&gt;
&lt;td&gt;Your infrastructure&lt;/td&gt;
&lt;td&gt;Handled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Free (+ server costs)&lt;/td&gt;
&lt;td&gt;Free (+ server costs)&lt;/td&gt;
&lt;td&gt;Free tier, then paid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try Screenshotrun
&lt;/h2&gt;

&lt;p&gt;If you want to try the API approach, &lt;a href="https://screenshotrun.com" rel="noopener noreferrer"&gt;Screenshotrun&lt;/a&gt; gives you 300 free screenshots per month. No credit card, no setup. Sign up, grab your API key from the dashboard, and paste it into any of the code examples above.&lt;/p&gt;

&lt;p&gt;The API also supports WebP output, dark mode, ad blocking, custom CSS injection, and webhooks for async rendering. &lt;a href="https://screenshotrun.com/docs" rel="noopener noreferrer"&gt;Full documentation here.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Take a Website Screenshot with PHP</title>
      <dc:creator>Vitalii Holben</dc:creator>
      <pubDate>Wed, 25 Mar 2026 00:15:06 +0000</pubDate>
      <link>https://dev.to/webmox/how-to-take-a-website-screenshot-with-php-12i</link>
      <guid>https://dev.to/webmox/how-to-take-a-website-screenshot-with-php-12i</guid>
      <description>&lt;p&gt;I want to walk you through the full process of taking website screenshots from PHP today. Not theory, not a library overview -- the actual thing. Open a terminal, install dependencies, write code, run it, get a PNG file with a screenshot. The whole path from an empty folder to a working result.&lt;/p&gt;

&lt;p&gt;Why would you even need this? Link previews for a directory site, automated OG images, screenshots for client reports, visual monitoring. Sooner or later a PHP project needs to turn a URL into an image.&lt;/p&gt;

&lt;p&gt;The problem is that PHP can't render web pages on its own. There's no browser engine built in. So we'll need a helper -- headless Chrome through Puppeteer. It's a Node.js package, and yes, that means we need Node alongside PHP. Sounds like overkill, but you'll see it's not that bad.&lt;/p&gt;

&lt;p&gt;Let's try it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Node.js
&lt;/h2&gt;

&lt;p&gt;First things first -- we need Node.js. Puppeteer won't run without it.&lt;/p&gt;

&lt;p&gt;If you're on macOS with Homebrew, one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Ubuntu it's a bit longer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_20.x | &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; bash -
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick check that everything installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt;
npm &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vr54v87co3tjb3mf40c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vr54v87co3tjb3mf40c.png" alt=" " width="800" height="205"&gt;&lt;/a&gt;&lt;br&gt;
See version numbers? Good, moving on.&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating the project and installing Puppeteer
&lt;/h2&gt;

&lt;p&gt;Now we need a separate folder for our screenshot tool. I usually put it next to the PHP project, but it doesn't really matter where.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;screenshot-tool
&lt;span class="nb"&gt;cd &lt;/span&gt;screenshot-tool
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;puppeteer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will take a minute. Puppeteer downloads a full Chromium binary during installation -- somewhere between 170 and 400 megabytes depending on your OS. Yeah, it's a chunky package. That's the price of getting a real browser you can control from code.&lt;/p&gt;

&lt;p&gt;If you're on Ubuntu, Chrome might fail to launch the first time -- it needs system libraries that aren't there on a fresh install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; libnss3 libatk1.0-0 libatk-bridge2.0-0 &lt;span class="se"&gt;\&lt;/span&gt;
    libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 &lt;span class="se"&gt;\&lt;/span&gt;
    libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS you can skip this step -- everything works out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the screenshot script
&lt;/h2&gt;

&lt;p&gt;Now for the fun part. Create a file called &lt;code&gt;screenshot.js&lt;/code&gt; inside the &lt;code&gt;screenshot-tool&lt;/code&gt; folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;puppeteer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshot.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Usage: node screenshot.js &amp;lt;url&amp;gt; [output-file]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--no-sandbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--disable-setuid-sandbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setViewport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fullPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does: takes a URL from the command line arguments, launches headless Chrome, opens the page, waits for it to load, takes a screenshot, saves it to a file. Then closes the browser.&lt;/p&gt;

&lt;p&gt;Let's test it right away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node screenshot.js https://screenshotrun.com test.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything worked, you'll see a new &lt;code&gt;test.png&lt;/code&gt; file in the folder. Open it -- if you see a screenshot of the page, the script works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dpv98vv3quwj35abo2v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dpv98vv3quwj35abo2v.png" alt=" " width="800" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting it to PHP
&lt;/h2&gt;

&lt;p&gt;The script works from the terminal. Now we need PHP to call it. The idea is simple: PHP runs our Node script through &lt;code&gt;shell_exec()&lt;/code&gt; like any other console command, and picks up the resulting PNG from disk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzoc30218hirhbbsyqkcg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzoc30218hirhbbsyqkcg.png" alt=" " width="800" height="578"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://screenshotrun.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$outputDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/screenshots'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.png'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$outputFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$outputDir&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$scriptPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/../screenshot-tool/screenshot.js'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'node %s %s %s 2&amp;gt;&amp;amp;1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;escapeshellarg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scriptPath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nb"&gt;escapeshellarg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nb"&gt;escapeshellarg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$outputFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$outputFile&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Done! Saved to: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$outputFile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"File size: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;filesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$outputFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" KB&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Something went wrong.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Output: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; at the end of the command -- it redirects stderr to stdout. Without it, if Node throws an error, PHP simply won't see it and you'll be left guessing why nothing works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding options
&lt;/h2&gt;

&lt;p&gt;The basic version works, but let's make it more useful. Here's a wrapper function with viewport size and full-page support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$outputDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$fullPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$width&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$height&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fullPage&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.png'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$outputFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;rtrim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$outputDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$scriptPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/../screenshot-tool/screenshot.js'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'node %s %s %s %d %d %s 2&amp;gt;&amp;amp;1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;escapeshellarg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scriptPath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nb"&gt;escapeshellarg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nb"&gt;escapeshellarg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$outputFile&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nv"&gt;$width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$fullPage&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'false'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$outputFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$outputFile&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Desktop screenshot&lt;/span&gt;
&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://screenshotrun.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/screenshots'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"Desktop: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Failed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Mobile (iPhone-sized)&lt;/span&gt;
&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://screenshotrun.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/screenshots'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;375&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;812&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"Mobile: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Failed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Full page with scrolling&lt;/span&gt;
&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;takeScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://screenshotrun.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/screenshots'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"Full page: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Failed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can take desktop, mobile, and full-page screenshots with one function call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0tvcu3vkejxo3zyefaoj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0tvcu3vkejxo3zyefaoj.png" alt=" " width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  There's a simpler way
&lt;/h2&gt;

&lt;p&gt;We went through the whole thing: Node.js, Puppeteer with Chromium, a JS script, shell_exec from PHP, a wrapper with options. It works -- but it's a lot of moving parts for one task.&lt;/p&gt;

&lt;p&gt;You can get the same result with a single HTTP request. No Node.js, no Chromium, no two scripts in two languages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="nv"&gt;$apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'YOUR_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;curl_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://screenshotrun.com/api/v1/screenshots'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;curl_setopt_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="no"&gt;CURLOPT_POST&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;CURLOPT_HTTPHEADER&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'Authorization: Bearer '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="no"&gt;CURLOPT_POSTFIELDS&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'url'&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://screenshotrun.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'format'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'png'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'block_cookies'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="no"&gt;CURLOPT_RETURNTRANSFER&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;curl_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;curl_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Screenshot ID: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'data'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I built &lt;a href="https://screenshotrun.com" rel="noopener noreferrer"&gt;ScreenshotRun&lt;/a&gt; as a simpler alternative — one HTTP request instead of managing Puppeteer. Free tier gives you 300 screenshots/month.&lt;/p&gt;

&lt;p&gt;If you're using AI coding tools like Claude or Cursor, there's also an &lt;a href="https://www.npmjs.com/package/screenshotrun-mcp" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; that lets your AI agent take screenshots directly.&lt;/p&gt;

</description>
      <category>php</category>
      <category>screenshot</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
