<?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: Grabbit</title>
    <description>The latest articles on DEV Community by Grabbit (grabbit).</description>
    <link>https://dev.to/grabbit</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.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F13692%2F644a5de6-bf00-4ad6-907f-873e90cb3bd7.png</url>
      <title>DEV Community: Grabbit</title>
      <link>https://dev.to/grabbit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/grabbit"/>
    <language>en</language>
    <item>
      <title>How to Screenshot a List of URLs (Bulk Capture for Research and Audits)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Tue, 07 Jul 2026 11:40:47 +0000</pubDate>
      <link>https://dev.to/grabbit/how-to-screenshot-a-list-of-urls-bulk-capture-for-research-and-audits-1039</link>
      <guid>https://dev.to/grabbit/how-to-screenshot-a-list-of-urls-bulk-capture-for-research-and-audits-1039</guid>
      <description>&lt;p&gt;You have a spreadsheet, a sitemap, or a list of competitor pages, and you need a screenshot of each one. Doing it by hand means opening twenty tabs and pressing the capture key twenty times. This guide covers every method that actually works, from a free browser extension for a one-off batch to a scripted loop you can run on a schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;To screenshot a list of URLs, &lt;strong&gt;loop over the list and capture each URL once&lt;/strong&gt;. There is no magic "capture them all in one shot" primitive. Every tool, extension, or API does the same thing under the hood: it visits each page in turn and renders it. The real choice is &lt;em&gt;how&lt;/em&gt; you drive that loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-off, a handful of URLs:&lt;/strong&gt; a browser extension that reads a pasted list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatable, or part of a pipeline:&lt;/strong&gt; a short script that calls a screenshot API once per URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You already write capture code:&lt;/strong&gt; keep your headless browser, just point the loop at a managed renderer if local Chromium is flaky in CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Method 1: a browser extension (for a quick one-off)
&lt;/h2&gt;

&lt;p&gt;If you just need to grab a dozen pages once and never again, a Chrome extension like &lt;strong&gt;Multiple Screenshots&lt;/strong&gt; is the fastest path. You paste your list, it opens each page, captures it, and saves the files locally. No code.&lt;/p&gt;

&lt;p&gt;The limits show up the moment the job repeats. Extensions run in your browser, on your machine, with your session. You cannot put that in CI, schedule it overnight, or hand it to a teammate as a reproducible step. For a true one-time audit, fine. For anything you will do again, it is a dead end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: an open-source script (DIY headless)
&lt;/h2&gt;

&lt;p&gt;The classic developer answer is a script that drives a headless browser over the list. Tools like &lt;code&gt;webscreenshot&lt;/code&gt; or a few lines of Puppeteer read a file of URLs and capture each one:&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;import&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="k"&gt;from&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node: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;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urls.txt&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;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&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;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;for &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="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;urls&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="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="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="s2"&gt;`out/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&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;.png`&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="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;p&gt;This works, and it is free. What it does not solve is everything &lt;em&gt;around&lt;/em&gt; the capture: keeping Chromium installed and patched, handling pages that block headless browsers, dealing with cookie banners, and surviving in a CI runner or serverless function where a real browser binary is awkward to ship. Those problems scale with the size of your list. See &lt;a href="https://www.grabbit.live/blog/screenshot-from-url" rel="noopener noreferrer"&gt;screenshot a website from a URL&lt;/a&gt; for a fuller comparison of the DIY path versus an API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: a screenshot API in a loop (repeatable)
&lt;/h2&gt;

&lt;p&gt;When the job has to run more than once, a screenshot API turns each capture into a single HTTP request, so the loop is the whole program. You send a URL, you get back a hosted image. No browser to install, nothing to keep patched.&lt;/p&gt;

&lt;p&gt;Here is the full pattern in Node. Read the list, capture each URL, and collect the hosted image URLs:&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;urls&lt;/span&gt; &lt;span class="o"&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;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://stripe.com&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;https://vercel.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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;for &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="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;urls&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;res&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.grabbit.live/v1/grabs&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&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 sk_live_...&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;Content-Type&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;application/json&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="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;full_page&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="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="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;results&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image_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;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each response includes a hosted &lt;code&gt;image_url&lt;/code&gt;, so you can drop the results straight into a CSV, a database, or a dashboard. Because every request is independent, you can keep the loop sequential for simplicity, or add limited concurrency if you want the batch to finish faster. This is exactly the kind of repeatable, scriptable job &lt;a href="https://www.grabbit.live/automated-screenshots" rel="noopener noreferrer"&gt;Grabbit's automated screenshots&lt;/a&gt; are built for, and you can wire it up with a free test key before adding a card.&lt;/p&gt;

&lt;p&gt;A note on honesty: Grabbit does not expose a single "send me an array of URLs" endpoint. One request captures one URL. The loop above &lt;em&gt;is&lt;/em&gt; the bulk pattern, and it keeps each capture independent so one bad URL never sinks the whole batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing per-URL options
&lt;/h2&gt;

&lt;p&gt;Each call in the loop is configured on its own, so a mixed list is no problem. The parameters that matter for a bulk job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;full_page: true&lt;/code&gt;&lt;/strong&gt; captures the entire scrollable page, not just the viewport. Essential for audits where you want the whole page on record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;width&lt;/code&gt;&lt;/strong&gt; sets the viewport (320 to 1920). Use a consistent width across the list so the screenshots line up for comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;format&lt;/code&gt;&lt;/strong&gt; can be &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;jpeg&lt;/code&gt;, or &lt;code&gt;webp&lt;/code&gt;. WebP keeps a large batch small on disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;delay_ms&lt;/code&gt;&lt;/strong&gt; waits up to 10 seconds before the capture, which helps on pages that fade content in after load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a large list where you do not want to hold the connection open per request, the same endpoint accepts &lt;code&gt;Prefer: respond-async&lt;/code&gt; (or &lt;code&gt;?async=true&lt;/code&gt;) and returns immediately with a job you poll later. That keeps a thousand-URL run from blocking your script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bulk screenshots for audits and monitoring
&lt;/h2&gt;

&lt;p&gt;The reason most people want to screenshot a list of URLs is not the screenshots themselves, it is the comparison. Capture your competitors' landing pages today, capture them again next month, and the diff tells a story. Save each &lt;code&gt;image_url&lt;/code&gt; next to its source URL and a timestamp, and you have the raw material for &lt;a href="https://www.grabbit.live/blog/website-change-monitoring" rel="noopener noreferrer"&gt;website change monitoring&lt;/a&gt; without building a crawler from scratch.&lt;/p&gt;

&lt;p&gt;The same loop powers a few common jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SEO audits:&lt;/strong&gt; snapshot every page in a sitemap to spot rendering or layout regressions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitor tracking:&lt;/strong&gt; re-run the same list on a schedule and compare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA evidence:&lt;/strong&gt; capture key pages on every deploy and attach them to the release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Screenshotting a list of URLs always comes down to a loop. A browser extension covers a one-time batch; a DIY script works until local Chromium becomes the maintenance burden; a screenshot API turns the whole job into a few lines that run anywhere, including CI and serverless. Point the loop at your list, collect the hosted image URLs, and you have a repeatable bulk-capture pipeline. To automate it end to end, see &lt;a href="https://www.grabbit.live/automated-screenshots" rel="noopener noreferrer"&gt;Grabbit's automated screenshots&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/screenshot-list-of-urls" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Screenshots for AI Agents: Giving Your LLM Eyes on the Web</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Mon, 06 Jul 2026 11:39:03 +0000</pubDate>
      <link>https://dev.to/grabbit/screenshots-for-ai-agents-giving-your-llm-eyes-on-the-web-2nee</link>
      <guid>https://dev.to/grabbit/screenshots-for-ai-agents-giving-your-llm-eyes-on-the-web-2nee</guid>
      <description>&lt;p&gt;Most LLMs process text. But a growing category of agent tasks is fundamentally visual: checking whether a deploy broke a page layout, verifying that an OG card looks right before a post goes live, monitoring a competitor's pricing page for changes. Text cannot do these jobs. A screenshot can.&lt;/p&gt;

&lt;p&gt;The pattern is straightforward: capture a screenshot via API, get back a hosted image URL, pass that URL to a vision model. No headless browser to manage, no Puppeteer session to keep alive, no parsing fragile DOM trees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why text-only agents hit a wall
&lt;/h2&gt;

&lt;p&gt;HTML is a poor proxy for what a user sees. Dynamic frameworks render client-side; the raw HTML is a template with no content. CSS controls what is visible and what is not. Interstitials, cookie banners, and lazy-loaded images change the actual visual state. A text-based agent scraping HTML sees none of this.&lt;/p&gt;

&lt;p&gt;Vision models bypass the problem entirely. They look at the page the way a user does. The prerequisite: they need an image to analyze. That is what the screenshot API provides.&lt;/p&gt;

&lt;h2&gt;
  
  
  The capture-and-analyze pattern
&lt;/h2&gt;

&lt;p&gt;The core loop for a vision-capable agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;seeUrl&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;res&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.grabbit.live/v1/grabs&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GRABBIT_API_KEY&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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;application/json&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="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="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="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;grab&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;grab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// a hosted URL, ready to pass to any vision model&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function returns a URL your agent passes directly to whatever vision model it uses as an image input. The image is already hosted; the vision model fetches it. No base64 encoding, no file handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture params that matter for agents
&lt;/h2&gt;

&lt;p&gt;Three params make the most difference in agentic contexts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;selector&lt;/code&gt;&lt;/strong&gt; waits for a specific element to appear in the DOM, then captures just that element. Useful when you care about one component, not the whole page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://yoursite.com/dashboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#revenue-chart"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returns an image of the &lt;code&gt;#revenue-chart&lt;/code&gt; element. Pass that to a vision model to read a number, detect a visual anomaly, or compare it to a previous capture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;full_page&lt;/code&gt;&lt;/strong&gt; captures the entire document height, not just the initial viewport. Set this when your agent needs to see content that loads below the fold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;delay_ms&lt;/code&gt;&lt;/strong&gt; adds a fixed wait after page load. If the page fetches data from an API and renders it into the DOM, a &lt;code&gt;delay_ms&lt;/code&gt; of 500 to 1000 gives the script time to finish before the capture fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic use cases
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Automated visual QA.&lt;/strong&gt; After each deploy, an agent screenshots key pages and passes them to a vision model with a prompt describing what to look for. The model spots a broken nav, a missing hero image, or an overlapping button before a human does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OG card verification.&lt;/strong&gt; Before a post goes live, an agent screenshots the &lt;code&gt;/og?title=...&lt;/code&gt; route at 1200 by 630, passes the image to a vision model, and confirms the title, author, and brand logo are all present and not clipped. See &lt;a href="https://www.grabbit.live/blog/og-image-generator" rel="noopener noreferrer"&gt;How to Generate Dynamic OG Images from Any URL&lt;/a&gt; for the template pattern this builds on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring competitor pages.&lt;/strong&gt; Capture a competitor's pricing page on a schedule, then diff the image against the previous capture using a vision model. An alert fires when the layout or text changes significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual data extraction.&lt;/strong&gt; Some dashboards render data into charts with no accessible API. A screenshot of the chart, analyzed by a vision model, extracts the numbers without reverse-engineering a private API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering the tool in an agent
&lt;/h2&gt;

&lt;p&gt;The pattern is the same across frameworks: define a function tool that accepts a URL and returns the &lt;code&gt;image_url&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;screenshotTool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&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_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Capture a screenshot of a web page and return a hosted image URL for visual analysis.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&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;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&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="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;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The page to screenshot.&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;selector&lt;/span&gt;&lt;span class="p"&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;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Optional CSS selector. Waits for this element and captures it.&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;full_page&lt;/span&gt;&lt;span class="p"&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;boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Capture the full document height.&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;delay_ms&lt;/span&gt;&lt;span class="p"&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;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Milliseconds to wait after page load before capturing.&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="na"&gt;required&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;url&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;When the agent framework calls this tool and the function returns &lt;code&gt;image_url&lt;/code&gt;, include it in the next LLM message as an image block. The model sees the page and can reason about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  From agent tool to MCP server
&lt;/h2&gt;

&lt;p&gt;If you use an MCP-compatible host, you can expose screenshot capability as an MCP tool. The agent calls &lt;code&gt;screenshot_url&lt;/code&gt;, gets back an image URL, and uses it in the next turn. Grabbit is designed for exactly this: one API key, one endpoint, no browser runtime to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go further
&lt;/h2&gt;

&lt;p&gt;For running this pattern on a full page set on a CI schedule, &lt;a href="https://www.grabbit.live/blog/visual-regression-testing" rel="noopener noreferrer"&gt;Visual Regression Testing: A Practical Guide&lt;/a&gt; covers the screenshot-and-compare approach in depth.&lt;/p&gt;

&lt;p&gt;For capturing any URL on demand from your own code, see the &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;Grabbit screenshot API&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/screenshots-for-ai-agents" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>startup</category>
      <category>saas</category>
    </item>
    <item>
      <title>How to Take a Website Screenshot in Node.js (4 Ways)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Sun, 05 Jul 2026 11:37:11 +0000</pubDate>
      <link>https://dev.to/grabbit/how-to-take-a-website-screenshot-in-nodejs-4-ways-8h3</link>
      <guid>https://dev.to/grabbit/how-to-take-a-website-screenshot-in-nodejs-4-ways-8h3</guid>
      <description>&lt;p&gt;Node.js has no shortage of ways to screenshot a website. The honest list is four: Puppeteer, Playwright, a thin wrapper like &lt;code&gt;capture-website&lt;/code&gt;, or a hosted API. The first three all run Chromium on your server; the fourth does not. This guide shows each with working code and the trade-off that decides which one you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Puppeteer (the default)
&lt;/h2&gt;

&lt;p&gt;Puppeteer is the most common choice and drives headless Chrome directly:&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;import&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="k"&gt;from&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;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://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;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;example.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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail that matters is &lt;code&gt;waitUntil: 'networkidle0'&lt;/code&gt;, which waits for the network to settle before capturing so you do not screenshot a half-loaded page. &lt;code&gt;fullPage: true&lt;/code&gt; captures the whole scrolling page. For elements, quality, and the lazy-load gotchas, see &lt;a href="https://www.grabbit.live/blog/puppeteer-screenshot" rel="noopener noreferrer"&gt;full-page screenshots in Puppeteer&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Playwright (cross-browser)
&lt;/h2&gt;

&lt;p&gt;Playwright has nearly identical code and adds Firefox and WebKit with one API:&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;import&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="k"&gt;from&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;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://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;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;example.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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical wins over Puppeteer are auto-waiting (less timing code) and being able to capture the same page in three engines, including the engine behind Safari. See &lt;a href="https://www.grabbit.live/blog/playwright-screenshot" rel="noopener noreferrer"&gt;screenshots in Playwright&lt;/a&gt; for the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. capture-website (the short wrapper)
&lt;/h2&gt;

&lt;p&gt;If you just want one screenshot and do not want to manage a browser lifecycle, &lt;code&gt;capture-website&lt;/code&gt; wraps Puppeteer into one 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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;captureWebsite&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;capture-website&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;captureWebsite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;example.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="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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It still installs and runs Chromium under the hood, so the operational cost is the same as Puppeteer. It just hides the boilerplate. Avoid older packages like &lt;code&gt;node-webshot&lt;/code&gt; that wrap the abandoned PhantomJS; do not build new code on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost the first three share
&lt;/h2&gt;

&lt;p&gt;All three options above mean you run a browser on your server. None of the hard parts are the screenshot call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provisioning Chromium.&lt;/strong&gt; It needs a long list of system libraries. Slim containers and serverless functions hit missing-dependency errors before the first capture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory and zombie processes.&lt;/strong&gt; A browser not closed on every error path leaks memory and orphans processes under load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patching and scaling.&lt;/strong&gt; Chromium ships security updates constantly, and one browser handles one job at a time, so volume means a pool and a queue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If screenshots are a production feature rather than a script, this is infrastructure you now own.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A hosted API (no browser to run)
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt; runs the browser fleet for you. From Node.js it is one &lt;code&gt;fetch&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="nx"&gt;res&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.grabbit.live/v1/grabs&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&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 sk_live_...&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;Content-Type&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;application/json&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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://example.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="mi"&gt;1280&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="kc"&gt;true&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="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;image_url&lt;/span&gt; &lt;span class="p"&gt;}&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is a hosted image URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grb_01jx..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.grabbit.live/grabs/grb_01jx....webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1180&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library options map onto request parameters: viewport is &lt;code&gt;width&lt;/code&gt; (320 to 1920) and &lt;code&gt;height&lt;/code&gt; (240 to 1080), full page is &lt;code&gt;full_page&lt;/code&gt;, the element pattern is a &lt;code&gt;selector&lt;/code&gt; field, and the manual wait becomes &lt;code&gt;delay_ms&lt;/code&gt; (0 to 10000). &lt;code&gt;format&lt;/code&gt; is &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;jpeg&lt;/code&gt;, or &lt;code&gt;webp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which to use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Already running Puppeteer or Playwright&lt;/strong&gt; for tests or scraping: add the screenshot call, you are done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A quick one-off script:&lt;/strong&gt; &lt;code&gt;capture-website&lt;/code&gt; is the least code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots are a production feature:&lt;/strong&gt; an API, so you do not operate Chromium in your Node service at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the framework deep dives, see &lt;a href="https://www.grabbit.live/blog/puppeteer-screenshot" rel="noopener noreferrer"&gt;screenshots in Puppeteer&lt;/a&gt; and &lt;a href="https://www.grabbit.live/blog/playwright-screenshot" rel="noopener noreferrer"&gt;screenshots in Playwright&lt;/a&gt;. If you are weighing hosted options, the &lt;a href="https://www.grabbit.live/blog/best-screenshot-api" rel="noopener noreferrer"&gt;honest comparison of screenshot APIs&lt;/a&gt; covers the trade-offs without the marketing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/nodejs-screenshot-website" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>node</category>
      <category>programming</category>
    </item>
    <item>
      <title>Playwright Visual Regression Testing: Catch UI Bugs in CI</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Sat, 04 Jul 2026 11:37:30 +0000</pubDate>
      <link>https://dev.to/grabbit/playwright-visual-regression-testing-catch-ui-bugs-in-ci-31eh</link>
      <guid>https://dev.to/grabbit/playwright-visual-regression-testing-catch-ui-bugs-in-ci-31eh</guid>
      <description>&lt;p&gt;Playwright has visual regression testing built in. One assertion, &lt;code&gt;toHaveScreenshot&lt;/code&gt;, captures the page, diffs it against a stored baseline, and fails the test when the UI changes unexpectedly. The setup is genuinely a few lines. The hard part is not the API; it is keeping baselines stable so CI catches real regressions instead of failing on font rendering. This guide covers both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core workflow: toHaveScreenshot
&lt;/h2&gt;

&lt;p&gt;A visual test is a normal Playwright test with one screenshot assertion:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;homepage looks correct&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="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&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="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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&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;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;homepage.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;On the first run, there is no baseline, so Playwright captures &lt;code&gt;homepage.png&lt;/code&gt; and saves it, and the test "fails" once to tell you a baseline was created. On every run after that, it captures a fresh screenshot, compares it pixel by pixel against the saved baseline, and passes only if they match within your threshold.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;toHaveScreenshot&lt;/code&gt; is the right assertion to reach for: it auto-waits for the page to stop changing and retries until the screenshot is stable, which removes a whole class of flaky-capture bugs. Prefer it over the older &lt;code&gt;toMatchSnapshot&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating baselines after an intentional change
&lt;/h2&gt;

&lt;p&gt;When you change the UI on purpose, the baseline is now wrong and the test should fail until you accept the new look. Regenerate baselines with a flag:&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;test&lt;/span&gt; &lt;span class="nt"&gt;--update-snapshots&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rewrites the baseline images for any test whose capture changed. Commit those images so the new baseline travels with the code in Git, and reviewers can see the visual diff in the pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem: CI flake
&lt;/h2&gt;

&lt;p&gt;The most common frustration is tests that pass locally and fail in CI. This is almost never a real regression. It is the rendering environment: a different OS, different fonts, a different GPU, or a different browser build produces sub-pixel anti-aliasing differences that fail an exact-match comparison.&lt;/p&gt;

&lt;p&gt;Two fixes, used together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Generate baselines in the CI environment.&lt;/strong&gt; Do not commit baselines captured on your Mac and expect them to match a Linux CI runner. Generate them inside the same container that runs the tests, commonly the official Playwright Docker image, so the rendering matches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Allow a small diff.&lt;/strong&gt; Set a tolerance so anti-aliasing noise does not fail the build:&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&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;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;homepage.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="na"&gt;maxDiffPixelRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// tolerate up to 1% differing pixels&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;maxDiffPixelRatio&lt;/code&gt; and &lt;code&gt;maxDiffPixels&lt;/code&gt; are the two knobs. Start strict and loosen only as far as you must to stop false failures, or you will mask real regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a screenshot API fits
&lt;/h2&gt;

&lt;p&gt;Playwright's &lt;code&gt;toHaveScreenshot&lt;/code&gt; is the right tool for asserting your own components and pages inside a Playwright suite. A &lt;a href="https://www.grabbit.live/automated-screenshots" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt; is not a replacement for it; it is a complementary capture layer for a few specific jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A stable, environment-independent baseline source.&lt;/strong&gt; Capturing the same URL through a hosted browser fleet gives you a consistent render that does not depend on each developer's machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capturing pages outside your test suite.&lt;/strong&gt; Monitoring a live production page or a third-party page you do not control, on a schedule, is a capture job, not a Playwright test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reference image of the real deployed page&lt;/strong&gt; to compare against, rather than a locally rendered one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a baseline capture as a single request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.grabbit.live/v1/grabs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com",
    "width": 1280,
    "full_page": true,
    "format": "png"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes a hosted image you can store as a reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grb_01jx..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.grabbit.live/grabs/grb_01jx....png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;96420&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1240&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Width accepts 320 to 1920, height 240 to 1080, &lt;code&gt;full_page&lt;/code&gt; captures the whole page, and &lt;code&gt;delay_ms&lt;/code&gt; (0 to 10000) lets content settle before the shot. To be clear about scope: Grabbit captures the images. The diffing, the assertions, and the pass/fail live in Playwright or your VRT tool of choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;toHaveScreenshot&lt;/code&gt; for component and page assertions inside your Playwright suite.&lt;/li&gt;
&lt;li&gt;Generate baselines in the CI container and allow a small &lt;code&gt;maxDiffPixelRatio&lt;/code&gt; so you catch real regressions, not font noise.&lt;/li&gt;
&lt;li&gt;Reach for a screenshot API when you need a consistent baseline source or to capture pages that live outside the test suite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the bigger picture, see the &lt;a href="https://www.grabbit.live/blog/visual-regression-testing" rel="noopener noreferrer"&gt;practical guide to visual regression testing&lt;/a&gt; and the &lt;a href="https://www.grabbit.live/blog/visual-regression-testing-tools" rel="noopener noreferrer"&gt;roundup of visual regression testing tools&lt;/a&gt;. For Playwright screenshot mechanics beyond testing, see &lt;a href="https://www.grabbit.live/blog/playwright-screenshot" rel="noopener noreferrer"&gt;taking screenshots in Playwright&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/playwright-visual-regression-testing" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Screenshot an Entire Web Page on Any Device</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Fri, 03 Jul 2026 11:38:47 +0000</pubDate>
      <link>https://dev.to/grabbit/how-to-screenshot-an-entire-web-page-on-any-device-5c54</link>
      <guid>https://dev.to/grabbit/how-to-screenshot-an-entire-web-page-on-any-device-5c54</guid>
      <description>&lt;p&gt;Full-page screenshots are straightforward on desktop but awkward on mobile, and totally different again when you need them from code. This guide covers the real methods for each situation so you can pick the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quickest cross-device method
&lt;/h2&gt;

&lt;p&gt;If you are writing code, running a CI job, or building a tool that needs to capture pages from a server or agent, a screenshot API is the only method that works everywhere. The browser runs in the cloud, not on your device.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.grabbit.live/v1/grabs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com",
    "width": 1280,
    "full_page": true,
    "format": "webp"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;full_page: true&lt;/code&gt; tells the renderer to scroll to the bottom of the page and stitch all the sections into one image. The response contains a hosted &lt;code&gt;image_url&lt;/code&gt; you can write directly to your database or pass to a front end. No browser install, no Puppeteer, no scrolling logic to maintain.&lt;/p&gt;

&lt;p&gt;For a broader look at programmatic capture, see &lt;a href="https://www.grabbit.live/blog/screenshot-from-url" rel="noopener noreferrer"&gt;how to screenshot a website from a URL&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  On desktop: Chrome DevTools
&lt;/h2&gt;

&lt;p&gt;Chrome includes a full-page screenshot command that requires no extensions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open DevTools: &lt;code&gt;F12&lt;/code&gt; on Windows or Linux, &lt;code&gt;Cmd + Opt + I&lt;/code&gt; on Mac.&lt;/li&gt;
&lt;li&gt;Open the command palette: &lt;code&gt;Ctrl + Shift + P&lt;/code&gt; on Windows, &lt;code&gt;Cmd + Shift + P&lt;/code&gt; on Mac.&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;full size screenshot&lt;/code&gt; and press Enter.&lt;/li&gt;
&lt;li&gt;Chrome saves a PNG of the full page to your Downloads folder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The capture uses the same Chromium engine as most screenshot APIs, so the result looks accurate. The limitation is that it only works from a desktop browser and cannot be scripted. For more detail on this method, see &lt;a href="https://www.grabbit.live/blog/capture-full-page-screenshot-chrome" rel="noopener noreferrer"&gt;how to capture a full-page screenshot in Chrome&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  On desktop: Firefox
&lt;/h2&gt;

&lt;p&gt;Firefox exposes its screenshot tool without DevTools.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Right-click anywhere on the page and choose &lt;strong&gt;Take Screenshot&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save full page&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Firefox downloads a PNG of the entire scrollable page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No keyboard shortcuts to remember and no extensions needed. The output is always PNG.&lt;/p&gt;

&lt;h2&gt;
  
  
  On Mac: Safari
&lt;/h2&gt;

&lt;p&gt;Safari has no native full-page screenshot shortcut. Your options are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use Chrome on the same machine (the DevTools method above).&lt;/li&gt;
&lt;li&gt;Install a browser extension such as GoFullPage or Nimbus Capture.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Cmd + Shift + 4&lt;/code&gt; to capture the visible area, then scroll and repeat (tedious and imprecise).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Safari is a hard requirement, a browser extension or the API is the path of least resistance.&lt;/p&gt;

&lt;h2&gt;
  
  
  On Windows: built-in tools
&lt;/h2&gt;

&lt;p&gt;The Windows Snipping Tool (&lt;code&gt;Win + Shift + S&lt;/code&gt;) captures the visible viewport only. There is no built-in way to scroll and stitch from the OS level. Use the Chrome DevTools method or a browser extension to get the full page.&lt;/p&gt;

&lt;h2&gt;
  
  
  On iPhone (iOS)
&lt;/h2&gt;

&lt;p&gt;iOS 13 and later include a built-in full-page screenshot option inside Safari.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take a regular screenshot: Side button + Volume Up on Face ID iPhones, or Home + Power on older models.&lt;/li&gt;
&lt;li&gt;Tap the thumbnail that appears in the lower-left corner.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Full Page&lt;/strong&gt; at the top of the edit view.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Done&lt;/strong&gt; and save to Files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two things to know: the capture is saved as a &lt;strong&gt;PDF&lt;/strong&gt;, not a PNG or WebP, and this only works in Safari, not Chrome or Firefox on iOS. If you need an actual image file, use a browser extension or the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  On Android
&lt;/h2&gt;

&lt;p&gt;Android does not have a single universal answer here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Samsung Galaxy:&lt;/strong&gt; Take a screenshot normally (Power + Volume Down), then tap &lt;strong&gt;Scroll capture&lt;/strong&gt; in the overlay that appears at the bottom. Samsung stitches the scrolled sections automatically into one image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Pixel and most stock Android:&lt;/strong&gt; No built-in scrolling screenshot. The closest native option is Share &amp;gt; Print &amp;gt; Save as PDF in Chrome, which has the same PDF limitation as iOS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All Android browsers:&lt;/strong&gt; Several extensions for Chrome on Android (such as GoFullPage) add a one-tap full-page screenshot. This is the practical choice on non-Samsung devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Choosing the right method
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Best method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Desktop, one-off manual capture&lt;/td&gt;
&lt;td&gt;Chrome DevTools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop, Firefox user&lt;/td&gt;
&lt;td&gt;Firefox right-click &amp;gt; Save full page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPhone, one-off&lt;/td&gt;
&lt;td&gt;iOS Safari full page (saves as PDF)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Samsung Android&lt;/td&gt;
&lt;td&gt;Scroll capture in the screenshot overlay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other Android&lt;/td&gt;
&lt;td&gt;Browser extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server, script, or CI&lt;/td&gt;
&lt;td&gt;Screenshot API (&lt;code&gt;full_page: true&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile device, need an image file&lt;/td&gt;
&lt;td&gt;Screenshot API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI agent or automated workflow&lt;/td&gt;
&lt;td&gt;Screenshot API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern that works everywhere and in every format: the API. Native browser tools are fine for manual one-off captures but differ by platform, change with browser updates, and cannot be automated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Chrome DevTools and Firefox cover desktop. iOS gives you full pages but only as PDFs in Safari. Android is split: Samsung has scrolling capture built in, everyone else needs an extension. For anything automated, cross-device, or outside a desktop browser, a screenshot API with &lt;code&gt;full_page: true&lt;/code&gt; is the consistent answer. Start capturing full pages programmatically with &lt;a href="https://www.grabbit.live/guides/screenshot-a-website" rel="noopener noreferrer"&gt;Grabbit's screenshot API&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/screenshot-entire-web-page" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Screenshot a Website in Python (3 Approaches That Work)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Thu, 02 Jul 2026 11:38:07 +0000</pubDate>
      <link>https://dev.to/grabbit/how-to-screenshot-a-website-in-python-3-approaches-that-work-gg2</link>
      <guid>https://dev.to/grabbit/how-to-screenshot-a-website-in-python-3-approaches-that-work-gg2</guid>
      <description>&lt;p&gt;There are three honest ways to screenshot a website in Python: drive Playwright, drive Selenium, or skip the browser and call an API. The code differs, but the hard parts are the same in all three: waiting for the page to be ready and capturing the full scrolling page, not just the viewport. This guide shows each approach with working code and the gotcha that bites people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: Playwright (the best default)
&lt;/h2&gt;

&lt;p&gt;Playwright for Python launches a browser, and a full-page screenshot is one parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&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="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;example.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&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;p&gt;Install it with &lt;code&gt;pip install playwright&lt;/code&gt; then &lt;code&gt;playwright install chromium&lt;/code&gt;. The reason to reach for Playwright first: it auto-waits for the page to reach a stable state before the screenshot fires, so you write less timing code, and &lt;code&gt;full_page=True&lt;/code&gt; handles the entire scrolling page in one line. It also drives Chromium, Firefox, and WebKit with the same API. See &lt;a href="https://www.grabbit.live/blog/playwright-screenshot" rel="noopener noreferrer"&gt;screenshots in Playwright&lt;/a&gt; for elements, CI, and the full-page edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: Selenium (when you already use it)
&lt;/h2&gt;

&lt;p&gt;Selenium captures the current window. In Python that is one method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;driver&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;example.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch: &lt;code&gt;save_screenshot&lt;/code&gt; captures only the visible viewport, and Selenium has no native full-page flag. For the whole page you drive the Chrome DevTools Protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_cdp_cmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Page.captureScreenshot&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;captureBeyondViewport&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fromSurface&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;full.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&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 more code and more browser-specific than Playwright, which is why Selenium is the right pick mainly when you already run a Selenium suite. The full breakdown is in &lt;a href="https://www.grabbit.live/blog/selenium-screenshot" rel="noopener noreferrer"&gt;screenshots in Selenium&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha both share: waiting
&lt;/h2&gt;

&lt;p&gt;The most common bug in either library is capturing before the page is ready. Do not use a fixed &lt;code&gt;time.sleep&lt;/code&gt;; wait on something real. In Selenium:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support.ui&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebDriverWait&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;expected_conditions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;EC&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.common.by&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;By&lt;/span&gt;

&lt;span class="nc"&gt;WebDriverWait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;EC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;presence_of_element_located&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;main-content&lt;/span&gt;&lt;span class="sh"&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;Playwright auto-waits for most cases, but for a specific element use &lt;code&gt;page.wait_for_selector('#main-content')&lt;/code&gt; before capturing. For web fonts, wait on &lt;code&gt;document.fonts.ready&lt;/code&gt; so the image does not show a fallback font.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 3: Skip the browser, call an API
&lt;/h2&gt;

&lt;p&gt;Both libraries above mean you install and operate a browser in Python: drivers, system libraries, memory management, and full-page workarounds. If screenshots are a production feature rather than a step in an existing script, a &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt; removes all of that. You send one HTTP request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.grabbit.live/v1/grabs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com",
    "width": 1280,
    "full_page": true,
    "format": "webp"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same call from Python with &lt;code&gt;requests&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.grabbit.live/v1/grabs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer sk_live_...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;'&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;full_page&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;webp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;image_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;image_url&lt;/span&gt;&lt;span class="sh"&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 response gives you a hosted image URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grb_01jx..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.grabbit.live/grabs/grb_01jx....webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1180&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser patterns map onto request parameters: viewport is &lt;code&gt;width&lt;/code&gt; (320 to 1920) and &lt;code&gt;height&lt;/code&gt; (240 to 1080), full page is &lt;code&gt;full_page&lt;/code&gt;, the element pattern is a &lt;code&gt;selector&lt;/code&gt; field, and the explicit wait becomes &lt;code&gt;delay_ms&lt;/code&gt; (0 to 10000). &lt;code&gt;format&lt;/code&gt; is &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;jpeg&lt;/code&gt;, or &lt;code&gt;webp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which to use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New code, want the least friction:&lt;/strong&gt; Playwright. Fast setup, auto-waiting, one-line full page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already running Selenium:&lt;/strong&gt; keep it, add the CDP call for full page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots are a production feature, not a script:&lt;/strong&gt; an API, so you do not operate a browser in Python at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the framework deep dives, see &lt;a href="https://www.grabbit.live/blog/playwright-screenshot" rel="noopener noreferrer"&gt;screenshots in Playwright&lt;/a&gt; and &lt;a href="https://www.grabbit.live/blog/selenium-screenshot" rel="noopener noreferrer"&gt;screenshots in Selenium&lt;/a&gt;. If you are weighing hosted options, the &lt;a href="https://www.grabbit.live/blog/best-screenshot-api" rel="noopener noreferrer"&gt;honest comparison of screenshot APIs&lt;/a&gt; covers the trade-offs without the marketing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/python-screenshot-website" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>node</category>
      <category>programming</category>
    </item>
    <item>
      <title>OG Image Examples: 10 Patterns Worth Copying (and How They Are Built)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Wed, 01 Jul 2026 11:37:36 +0000</pubDate>
      <link>https://dev.to/grabbit/og-image-examples-10-patterns-worth-copying-and-how-they-are-built-3hmg</link>
      <guid>https://dev.to/grabbit/og-image-examples-10-patterns-worth-copying-and-how-they-are-built-3hmg</guid>
      <description>&lt;p&gt;The OG image is the first thing anyone sees before they decide to click a shared link. The best ones are not the prettiest; they are the most legible at a glance and the easiest to produce at scale. This is a tour of ten patterns that consistently work, what makes each one effective, and the build approach behind it, so you can copy the result rather than the pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  What separates a good OG image from a bad one
&lt;/h2&gt;

&lt;p&gt;Before the examples, the rules they all follow. An OG image is usually viewed small, in a busy feed, for a fraction of a second:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One focal point.&lt;/strong&gt; Usually the page title in large type. If a reader has to hunt, you have lost them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High contrast and big text.&lt;/strong&gt; It will be shrunk to a thumbnail. Thin gray text on a white card disappears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-brand, not generic.&lt;/strong&gt; Color, logo, and typeface that match the site, so the card is recognizable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct dimensions.&lt;/strong&gt; 1200 by 630 pixels, the 1.91 to 1 ratio every major platform renders full-width. See &lt;a href="https://www.grabbit.live/blog/og-image-sizes" rel="noopener noreferrer"&gt;OG image sizes&lt;/a&gt; for the full breakdown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The patterns below are different ways to satisfy those rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  10 patterns worth copying
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Title-on-brand-gradient.&lt;/strong&gt; The page title in large type over the site's brand gradient, with a small logo in a corner. The workhorse pattern: instantly readable, instantly recognizable, trivial to template. Most developer tools and SaaS blogs use a version of this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Title plus author avatar.&lt;/strong&gt; Adds the author's name and photo beside the title. Works well for blogs and newsletters where the writer is part of the draw.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code snippet card.&lt;/strong&gt; A syntax-highlighted snippet of the actual code the post is about. Strong for engineering content because the preview previews the substance, not just the headline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Product screenshot inset.&lt;/strong&gt; The title alongside a real screenshot of the product or dashboard. The honest version of "show, do not tell," and a natural fit when the page is about a UI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Big-number stat.&lt;/strong&gt; One large metric ("3x faster," "10,000 captures") as the focal point. Great for case studies and launch posts where a number is the hook.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Category label plus title.&lt;/strong&gt; A small colored pill ("Guide," "Changelog") above the title, so the reader knows the content type before clicking. Good for sites with mixed content.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Minimal wordmark.&lt;/strong&gt; Just the logo and a short tagline on a clean background. Best as a strong site-wide default for pages that do not warrant a custom image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quote card.&lt;/strong&gt; A pulled quote in large type, attributed. Effective for interviews, essays, and testimonials.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Map or chart render.&lt;/strong&gt; For data or location pages, the actual visualization as the card. This is one that pure design tools struggle with, because the image needs to be a real render of dynamic content.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Live page top-shot.&lt;/strong&gt; The OG image is a clean screenshot of the top of the actual page. Zero separate design: the page is the preview. Ideal for landing pages and docs whose hero already looks good.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How these are actually built
&lt;/h2&gt;

&lt;p&gt;Patterns 1 through 8 are layouts: a title, maybe an avatar or a stat, on a branded background. The maintainable way to produce them per page is a single template that takes the page data as input, rather than designing each card by hand. You can render that template two ways: a code-based image renderer (such as a framework's built-in OG route), or by pointing a screenshot API at a template URL you build with your normal components. The &lt;a href="https://www.grabbit.live/blog/og-image-generator" rel="noopener noreferrer"&gt;dynamic OG image generator approach&lt;/a&gt; walks through the template-and-capture version end to end.&lt;/p&gt;

&lt;p&gt;Patterns 9 and 10 are different. A map render or a real screenshot of the live page is not a layout you can easily rebuild in a code renderer; it is the actual page. For those, a &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt; is the natural fit, because it renders the real URL in a browser and hands back the image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the "live page top-shot" (pattern 10)
&lt;/h2&gt;

&lt;p&gt;The simplest pattern to ship is also the one design tools cannot do: capture the real page. Point a screenshot API at the URL at OG dimensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.grabbit.live/v1/grabs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://yoursite.com/your-page",
    "width": 1200,
    "height": 630,
    "format": "png",
    "delay_ms": 500
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response gives you a hosted &lt;code&gt;image_url&lt;/code&gt; to drop straight into your &lt;code&gt;og:image&lt;/code&gt; tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grb_01jx..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.grabbit.live/grabs/grb_01jx....png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;73900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1210&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Width accepts 320 to 1920 and height 240 to 1080, so &lt;code&gt;1200&lt;/code&gt; by &lt;code&gt;630&lt;/code&gt; is in range. &lt;code&gt;delay_ms&lt;/code&gt; (0 to 10000) lets fonts and client-rendered content settle before the shot. Capture once per page, cache the &lt;code&gt;image_url&lt;/code&gt;, and serve it from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Pick the pattern that fits your content, but make it per-page and make it automatic. A custom card with the post's title beats a generic logo every time, and the only way that scales past a handful of pages is a template you render on demand or a screenshot of the page itself.&lt;/p&gt;

&lt;p&gt;New to the format? Start with &lt;a href="https://www.grabbit.live/blog/what-is-an-og-image" rel="noopener noreferrer"&gt;what is an OG image&lt;/a&gt;. Ready to build one? The &lt;a href="https://www.grabbit.live/blog/og-image-generator" rel="noopener noreferrer"&gt;dynamic OG image guide&lt;/a&gt; has the full template-and-capture flow.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/og-image-examples" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>seo</category>
      <category>html</category>
    </item>
    <item>
      <title>How to Monitor a Website for Visual Changes</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Tue, 30 Jun 2026 11:38:40 +0000</pubDate>
      <link>https://dev.to/grabbit/how-to-monitor-a-website-for-visual-changes-59n6</link>
      <guid>https://dev.to/grabbit/how-to-monitor-a-website-for-visual-changes-59n6</guid>
      <description>&lt;p&gt;A server returning 200 does not mean the page looks right. A deploy can push broken CSS that shifts every section, a CMS edit can overwrite a hero image, or an A/B test can activate on production and change copy you did not expect. Uptime monitoring misses all of this. Visual change monitoring catches it by comparing what the page actually looks like, not just whether it responded.&lt;/p&gt;

&lt;p&gt;The setup has three parts: capture a baseline screenshot, recapture on a schedule, and diff the two images. The rest is just wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  What visual change monitoring catches
&lt;/h2&gt;

&lt;p&gt;Uptime monitors check that a server responds and that the response is not an error page. They know nothing about layout or content. Visual monitoring fills the gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSS regressions&lt;/strong&gt; from a bad deploy: text overflows, sections collapse, buttons disappear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unintended content edits&lt;/strong&gt;: a CMS change that overwrote a heading or swapped an image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party widget breakage&lt;/strong&gt;: a chat widget or cookie banner that started covering half the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A/B test leakage&lt;/strong&gt;: a variant that activated outside its intended audience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitor price changes&lt;/strong&gt;: if you monitor external pages, any content change triggers an alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Visual monitoring is a catch-all for anything that changes pixels. That makes it useful both for your own production site and for tracking pages you do not control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The monitoring loop
&lt;/h2&gt;

&lt;p&gt;The loop is the same whether you build it yourself or use a hosted tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture a baseline.&lt;/strong&gt; Screenshot the page in a known good state and store the image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recapture on schedule.&lt;/strong&gt; At whatever interval matters, take a new screenshot of the same URL with the same settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff.&lt;/strong&gt; Compare the new capture to the baseline pixel by pixel (or perceptually).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert or pass.&lt;/strong&gt; If the diff is below your threshold, all is well. If it exceeds it, send an alert or fail a pipeline step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update the baseline.&lt;/strong&gt; When a change is intentional, approve it as the new baseline so it does not keep alerting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most common failure is an inconsistent capture, not a broken diff algorithm. Animations caught mid-flight, lazy-loaded content that had not settled, or a viewport that shifted between runs all produce false positives. Making the capture deterministic is the most important part of the setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the capture layer
&lt;/h2&gt;

&lt;p&gt;The capture needs to be consistent between runs: same viewport, same timing, same format. With a screenshot API you pin all of those at the call site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.grabbit.live/v1/grabs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com/pricing",
    "width": 1280,
    "full_page": true,
    "delay_ms": 800,
    "format": "png"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;width&lt;/code&gt; keeps the layout identical between runs. &lt;code&gt;full_page&lt;/code&gt; captures the entire document so a change anywhere on the page is visible. &lt;code&gt;delay_ms&lt;/code&gt; waits for content to settle after page load, which prevents lazy-loaded images and animations from registering as false positives. The response includes an &lt;code&gt;image_url&lt;/code&gt; pointing to the stored capture. Save that URL as your baseline; capture the same URL again on each check; feed both to a diff library.&lt;/p&gt;

&lt;p&gt;To monitor a specific component rather than the full page, add &lt;code&gt;"selector": "#pricing-table"&lt;/code&gt; and only that element is captured. The diff stays focused and noise from unrelated page sections disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diffing the captures
&lt;/h2&gt;

&lt;p&gt;Two common options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pixelmatch&lt;/strong&gt; (Node.js) compares PNG buffers pixel by pixel and returns a count of changed pixels. Fast, well-maintained, zero dependencies. Pair it with &lt;code&gt;pngjs&lt;/code&gt; to decode the image buffers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;resemblejs&lt;/strong&gt; does a perceptual comparison that is more tolerant of sub-pixel rendering differences. Better for pages with anti-aliased text where a strict pixel comparison produces noisy diffs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set a threshold that matches how sensitive the page is. A marketing landing page with lots of imagery needs a looser threshold than a data table with precise numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dedicated tools that bundle the loop
&lt;/h2&gt;

&lt;p&gt;If you prefer not to assemble the pieces yourself, these tools wrap capture, diff, and alerting together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visualping&lt;/strong&gt; handles any URL with a visual or text comparison and emails you when something changes. Suited for non-technical use cases like competitor monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hexowatch&lt;/strong&gt; adds advanced change types (source code, technology, availability) alongside visual monitoring, with Slack and webhook alerts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diffy&lt;/strong&gt; is built for comparing environments, typically staging versus production, on a schedule. Useful for confirming a deploy did not introduce a visual regression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;changedetection.io&lt;/strong&gt; is open source and self-hostable. It covers both visual and text changes, with a clean diff view and integration hooks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off with hosted tools is that you hand over the capture schedule and baseline storage. For a custom pipeline, you control all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduling and thresholds
&lt;/h2&gt;

&lt;p&gt;Run frequency depends on what you are monitoring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Production monitoring:&lt;/strong&gt; every 15 to 60 minutes is a common range. The check runs against a live URL and alerts your on-call channel via webhook if the diff exceeds the threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy-triggered checks:&lt;/strong&gt; trigger a check immediately after each deploy rather than on a fixed schedule. This is the tightest feedback loop for catching regressions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External pages:&lt;/strong&gt; daily checks are usually enough. If you are tracking a competitor's pricing page, a 24-hour delay is fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set the diff threshold by observing a known-stable page for a few days. Measure the typical drift from ads, avatars, and minor rendering variation. Use that as your floor, then set the alert threshold above it by a comfortable margin.&lt;/p&gt;

&lt;p&gt;For running captures at scale or across a list of URLs, see &lt;a href="https://www.grabbit.live/automated-screenshots" rel="noopener noreferrer"&gt;automated screenshots&lt;/a&gt; for the scheduling and parallelism patterns. For the capture options in detail, see the &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt;. If you want to build this into a full visual regression suite rather than production monitoring, &lt;a href="https://www.grabbit.live/blog/visual-regression-testing-tools" rel="noopener noreferrer"&gt;the best visual regression testing tools&lt;/a&gt; covers the open-source and SaaS options worth considering.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/website-change-monitoring" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Take Screenshots in Selenium (Full Page and Element)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Mon, 29 Jun 2026 11:38:34 +0000</pubDate>
      <link>https://dev.to/grabbit/how-to-take-screenshots-in-selenium-full-page-and-element-1em8</link>
      <guid>https://dev.to/grabbit/how-to-take-screenshots-in-selenium-full-page-and-element-1em8</guid>
      <description>&lt;p&gt;Selenium can take a screenshot in one line, but it captures only the visible viewport by default, and unlike Playwright or Puppeteer it has no built-in full-page flag. That gap is where most of the work hides. This guide covers the basic capture, the full-page workarounds Selenium actually needs, single-element shots, and the point where an API is less effort than driving a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic Selenium screenshot
&lt;/h2&gt;

&lt;p&gt;In Python, the driver captures the current window directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;driver&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;example.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Java, you cast the driver to &lt;code&gt;TakesScreenshot&lt;/code&gt; and choose an output type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;File&lt;/span&gt; &lt;span class="n"&gt;shot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nc"&gt;TakesScreenshot&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getScreenshotAs&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OutputType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FILE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both capture the visible viewport only. If the page is taller than the window, everything below the fold is missing. This is the single biggest difference from Puppeteer and Playwright, which both expose a one-line full-page option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full-page screenshots (the part Selenium does not do for you)
&lt;/h2&gt;

&lt;p&gt;Core Selenium has no &lt;code&gt;fullPage&lt;/code&gt; flag. You have three real options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Chrome DevTools Protocol.&lt;/strong&gt; Selenium 4 can send CDP commands. &lt;code&gt;Page.captureScreenshot&lt;/code&gt; with &lt;code&gt;captureBeyondViewport: true&lt;/code&gt; renders the entire page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_cdp_cmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Page.captureScreenshot&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;captureBeyondViewport&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fromSurface&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;full.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&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;strong&gt;2. Firefox's native method.&lt;/strong&gt; GeckoDriver exposes a full-page call that Chrome does not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_full_page_screenshot_as_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;full.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Firefox only
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Scroll and stitch.&lt;/strong&gt; Resize the window to the full scroll height, then capture. This is brittle on lazy-loaded pages and pages with &lt;code&gt;position: fixed&lt;/code&gt; headers, which repeat or float in the stitched result.&lt;/p&gt;

&lt;p&gt;The takeaway: full-page capture in Selenium is browser-specific and more code than the other tools. If full page is your main need, that is worth knowing before you build on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshotting a single element
&lt;/h2&gt;

&lt;p&gt;Call the screenshot method on the &lt;code&gt;WebElement&lt;/code&gt; instead of the driver. Selenium crops to the element's bounding box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.common.by&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;By&lt;/span&gt;

&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pricing-card&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;card&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;card.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the element is below the fold, scroll it into view first so it actually renders before the capture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arguments[0].scrollIntoView();&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Waiting so the capture is not blank
&lt;/h2&gt;

&lt;p&gt;The most common bug is capturing before the page is ready. Use an explicit wait keyed to something real on the page rather than a fixed &lt;code&gt;sleep&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support.ui&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebDriverWait&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;expected_conditions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;EC&lt;/span&gt;

&lt;span class="nc"&gt;WebDriverWait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;EC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;presence_of_element_located&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pricing-card&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ready.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For web fonts specifically, wait on &lt;code&gt;document.fonts.ready&lt;/code&gt; via &lt;code&gt;execute_script&lt;/code&gt;, otherwise the screenshot can show a fallback font.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where running Selenium just for screenshots gets expensive
&lt;/h2&gt;

&lt;p&gt;Selenium is a browser-testing framework. Using it only to produce images means you own everything that comes with driving a browser, none of which is the screenshot itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drivers and browsers.&lt;/strong&gt; You provision ChromeDriver or GeckoDriver, keep it matched to the browser version, and install the browser plus its system libraries in every environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-page workarounds.&lt;/strong&gt; The CDP or scroll-and-stitch code above is yours to maintain across browser updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory and concurrency.&lt;/strong&gt; A browser per job, closed on every error path, and a pool with back-pressure once you capture at volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If screenshots are the goal and the test suite is not, that is a lot of moving parts for an image.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same capture as an API call
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt; runs the browser for you. The full-page capture that took browser-specific code above becomes one request, with a real &lt;code&gt;full_page&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.grabbit.live/v1/grabs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com",
    "width": 1280,
    "full_page": true,
    "format": "webp"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes a hosted &lt;code&gt;image_url&lt;/code&gt; you can use directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grb_01jx..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.grabbit.live/grabs/grb_01jx....webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1180&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Selenium patterns map onto request parameters: viewport size is &lt;code&gt;width&lt;/code&gt; (320 to 1920) and &lt;code&gt;height&lt;/code&gt; (240 to 1080), full page is &lt;code&gt;full_page&lt;/code&gt;, the element-screenshot pattern is a &lt;code&gt;selector&lt;/code&gt; field, and the explicit wait becomes &lt;code&gt;delay_ms&lt;/code&gt; (0 to 10000). &lt;code&gt;format&lt;/code&gt; is &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;jpeg&lt;/code&gt;, or &lt;code&gt;webp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which to use
&lt;/h2&gt;

&lt;p&gt;Reach for Selenium when you already run a Selenium test suite and a screenshot is one assertion or artifact among many. Reach for an API when screenshots are the actual product feature, especially if you need full-page captures and would rather not maintain CDP calls and driver versions to get them.&lt;/p&gt;

&lt;p&gt;For the same comparison in other tools, see &lt;a href="https://www.grabbit.live/blog/puppeteer-screenshot" rel="noopener noreferrer"&gt;screenshots in Puppeteer&lt;/a&gt; and &lt;a href="https://www.grabbit.live/blog/playwright-screenshot" rel="noopener noreferrer"&gt;screenshots in Playwright&lt;/a&gt;. If you are weighing hosted options, the &lt;a href="https://www.grabbit.live/blog/best-screenshot-api" rel="noopener noreferrer"&gt;honest comparison of screenshot APIs&lt;/a&gt; covers the trade-offs without the marketing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/selenium-screenshot" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>node</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Add Dynamic OG Images in Next.js</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Sun, 28 Jun 2026 11:37:52 +0000</pubDate>
      <link>https://dev.to/grabbit/how-to-add-dynamic-og-images-in-nextjs-2kf8</link>
      <guid>https://dev.to/grabbit/how-to-add-dynamic-og-images-in-nextjs-2kf8</guid>
      <description>&lt;p&gt;Next.js has dynamic Open Graph images built in: drop an &lt;code&gt;opengraph-image.tsx&lt;/code&gt; file in a route segment and the framework generates the image and wires the meta tags. That covers most cases. The one it does not cover is when you want the OG image to be a real render of the page itself, not a separate JSX layout. This guide shows both: the native &lt;code&gt;@vercel/og&lt;/code&gt; approach for templated cards, and a screenshot API for capturing the actual page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The native approach: opengraph-image
&lt;/h2&gt;

&lt;p&gt;In the App Router, any route segment can export an OG image. A static file is the simplest version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
  blog/
    [slug]/
      opengraph-image.png   &amp;lt;- served automatically as the OG image
      page.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next.js detects &lt;code&gt;opengraph-image.png&lt;/code&gt; and adds the &lt;code&gt;og:image&lt;/code&gt; meta tags for that route. No code, no manual tags.&lt;/p&gt;

&lt;p&gt;For a dynamic card, use &lt;code&gt;opengraph-image.tsx&lt;/code&gt; and return an &lt;code&gt;ImageResponse&lt;/code&gt; from &lt;code&gt;next/og&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ImageResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/og&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&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;1200&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;630&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/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;export&lt;/span&gt; &lt;span class="k"&gt;default&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;Image&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="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;title&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;getPostTitle&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;100%&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;100%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0b1120&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;padding&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;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;size&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;The &lt;code&gt;size&lt;/code&gt; export is what makes this a correct OG image: &lt;code&gt;1200&lt;/code&gt; by &lt;code&gt;630&lt;/code&gt; is the 1.91 to 1 ratio every major platform renders as a full-width card. For the full breakdown of why this ratio matters, see &lt;a href="https://www.grabbit.live/blog/og-image-sizes" rel="noopener noreferrer"&gt;Open Graph image sizes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is the right tool when the card is a layout you control in JSX: a title, an author, a logo, a gradient. It runs on the edge, it is fast, and the image regenerates whenever the data changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where opengraph-image falls short
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@vercel/og&lt;/code&gt; renders a subset of CSS in an isolated environment. It does not render your actual page. That is a feature for simple cards, but it becomes a wall when the image you want is the page itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A dashboard or chart that already looks good and should appear in the preview as it really renders.&lt;/li&gt;
&lt;li&gt;A page built with components, fonts, or CSS features that the OG renderer does not support, so you would have to rebuild the layout twice.&lt;/li&gt;
&lt;li&gt;A marketing or docs page where the OG image should just be a clean shot of the top of the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In those cases you are no longer generating a card. You are taking a screenshot of a real page, and that is a different tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The screenshot approach: capture the real page
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt; renders the actual URL in a real browser and hands back a hosted image. You point it at the page (or a dedicated &lt;code&gt;/og&lt;/code&gt; route you build with your normal components) and use the result as your &lt;code&gt;og:image&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.grabbit.live/v1/grabs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://yourapp.com/og/some-post",
    "width": 1200,
    "height": 630,
    "format": "png",
    "delay_ms": 500
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes a hosted &lt;code&gt;image_url&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grb_01jx..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.grabbit.live/grabs/grb_01jx....png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;71240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1240&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Width accepts 320 to 1920 and height 240 to 1080, so &lt;code&gt;1200&lt;/code&gt; by &lt;code&gt;630&lt;/code&gt; is in range. The &lt;code&gt;delay_ms&lt;/code&gt; field (0 to 10000) gives client-rendered content or web fonts a moment to settle before the capture fires. Wire the returned &lt;code&gt;image_url&lt;/code&gt; into your metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&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;generateMetadata&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Metadata&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ogImageUrl&lt;/span&gt; &lt;span class="p"&gt;}&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;getPost&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;ogImageUrl&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;1200&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;630&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache the result: call the API once per unique page, store &lt;code&gt;image_url&lt;/code&gt; alongside the page record, and serve it from there so you are not capturing on every request. The same template-and-cache pattern, applied to your real page instead of a JSX card, is covered in &lt;a href="https://www.grabbit.live/blog/og-image-generator" rel="noopener noreferrer"&gt;how to generate dynamic OG images from any URL&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which to use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Templated card (title, author, brand), no need to match the page:&lt;/strong&gt; native &lt;code&gt;opengraph-image.tsx&lt;/code&gt; with &lt;code&gt;@vercel/og&lt;/code&gt;. It ships with Next.js and runs on the edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The OG image should be a real render of the page or app:&lt;/strong&gt; a screenshot API. You capture what users actually see, including content &lt;code&gt;@vercel/og&lt;/code&gt; cannot reproduce.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many apps use both: &lt;code&gt;@vercel/og&lt;/code&gt; for blog and marketing cards, a screenshot API for the pages that are too rich to rebuild as a card.&lt;/p&gt;

&lt;p&gt;Before you ship, confirm the tags resolve and the card looks right by testing the live URL. New to the format? Start with &lt;a href="https://www.grabbit.live/blog/what-is-an-og-image" rel="noopener noreferrer"&gt;what is an OG image&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/next-js-og-image" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>seo</category>
      <category>html</category>
    </item>
    <item>
      <title>How to Screenshot a Website from a URL (No Browser Needed)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Sat, 27 Jun 2026 11:40:33 +0000</pubDate>
      <link>https://dev.to/grabbit/how-to-screenshot-a-website-from-a-url-no-browser-needed-19h1</link>
      <guid>https://dev.to/grabbit/how-to-screenshot-a-website-from-a-url-no-browser-needed-19h1</guid>
      <description>&lt;p&gt;Every approach to capturing a screenshot from a URL falls into one of three categories: call an API, run a local headless browser, or use a client-side library. Each has a different setup cost, maintenance burden, and failure mode. Here is how to pick the right one and use it correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three approaches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Screenshot API.&lt;/strong&gt; You send a POST request with the target URL and receive a hosted image URL in the response. The headless browser runs in the cloud. No binaries to install, no drivers to version-match, no infrastructure to keep alive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Puppeteer or Playwright.&lt;/strong&gt; You launch headless Chrome locally, navigate to the URL, and call &lt;code&gt;.screenshot()&lt;/code&gt;. Full control, but you own the Chromium binary, the Node.js process, and every failure mode that comes with them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;html2canvas.&lt;/strong&gt; Runs in the browser and serializes what is already in the DOM to a canvas. Works only client-side and misses content loaded by JavaScript after the initial render.&lt;/p&gt;

&lt;p&gt;For server-side, automated, or high-volume capture, the API path is almost always the right call. For local dev scripts where you already have Node.js set up, Puppeteer and Playwright work well. html2canvas is the only viable option when you need a screenshot of what a user is looking at inside a browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using a screenshot API
&lt;/h2&gt;

&lt;p&gt;One POST request captures any public URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.grabbit.live/v1/grabs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com",
    "width": 1280,
    "height": 720,
    "format": "webp"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes a hosted &lt;code&gt;image_url&lt;/code&gt; you can store and serve directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grb_01jx..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.grabbit.live/grabs/grb_01jx....webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;62140&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;940&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-15T09:00:00.000Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Width&lt;/strong&gt; must be between 320 and 1920 pixels; &lt;strong&gt;height&lt;/strong&gt; between 240 and 1080. Format options are &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;jpeg&lt;/code&gt;, and &lt;code&gt;webp&lt;/code&gt; (webp is the best default for file size). Set &lt;code&gt;"full_page": true&lt;/code&gt; to capture the full scrollable height instead of the viewport. Add &lt;code&gt;"delay_ms": 1000&lt;/code&gt; if the page loads content client-side before it is visible.&lt;/p&gt;

&lt;p&gt;The same call in Python, no browser required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.grabbit.live/v1/grabs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer sk_live_...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webp&lt;/span&gt;&lt;span class="sh"&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No chromedriver, no binary downloads, no async process to tear down after the call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Puppeteer
&lt;/h2&gt;

&lt;p&gt;Puppeteer gives you direct control of Chromium at the cost of managing the binary yourself:&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;import&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="k"&gt;from&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;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://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;networkidle2&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;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;p&gt;Puppeteer ships Chromium at around 300 MB. It works well for local scripts, but deploying it to serverless runtimes or CI agents that lack the right system libraries is a recurring headache. See &lt;a href="https://www.grabbit.live/blog/puppeteer-screenshot" rel="noopener noreferrer"&gt;full-page screenshots with Puppeteer&lt;/a&gt; for the &lt;code&gt;fullPage: true&lt;/code&gt; option and the tradeoffs versus an API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Playwright
&lt;/h2&gt;

&lt;p&gt;Playwright's API is nearly identical and adds Firefox and WebKit 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="k"&gt;import&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="k"&gt;from&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;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;setViewportSize&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://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;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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright's &lt;code&gt;waitForSelector&lt;/code&gt; and &lt;code&gt;waitForLoadState&lt;/code&gt; options give it a slight edge for pages with delayed content. The infrastructure burden is the same as Puppeteer.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Best choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production automation or CI&lt;/td&gt;
&lt;td&gt;Screenshot API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless function&lt;/td&gt;
&lt;td&gt;Screenshot API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulk capture of many URLs&lt;/td&gt;
&lt;td&gt;Screenshot API with async mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local dev script or one-off audit&lt;/td&gt;
&lt;td&gt;Puppeteer or Playwright&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capture existing browser content (client-side)&lt;/td&gt;
&lt;td&gt;html2canvas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Adding a headless browser process to a server that already handles your application workload is a maintenance burden. The API separates the capture concern entirely and keeps your deployment small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capturing multiple URLs
&lt;/h2&gt;

&lt;p&gt;For bulk jobs, pass &lt;code&gt;Prefer: respond-async&lt;/code&gt; to queue the capture without waiting for it to finish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/about&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/pricing&lt;/span&gt;&lt;span class="sh"&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;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.grabbit.live/v1/grabs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer sk_live_...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prefer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;respond-async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# poll GET /api/v1/grabs/{id} until "done"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Prefer: respond-async&lt;/code&gt; returns &lt;code&gt;202&lt;/code&gt; immediately with a job ID. Poll &lt;code&gt;GET /api/v1/grabs/{id}&lt;/code&gt; until &lt;code&gt;status&lt;/code&gt; is &lt;code&gt;"done"&lt;/code&gt;, then read &lt;code&gt;image_url&lt;/code&gt;. You can queue up to 10 concurrent jobs per team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Waiting for dynamic content
&lt;/h2&gt;

&lt;p&gt;If the target page loads content after the initial HTML is served, a plain &lt;code&gt;page.goto&lt;/code&gt; or an instant API call may fire before the content is visible. Two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"delay_ms": 1500&lt;/code&gt; waits a fixed number of milliseconds after the page loads before capturing. Simple, but guesswork.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"selector": "#main-content"&lt;/code&gt; waits until that element is present in the DOM. Tied to a real content signal, not an arbitrary timer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For pure server-rendered pages with no client-side hydration, neither is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.grabbit.live/automated-screenshots" rel="noopener noreferrer"&gt;automated screenshots guide&lt;/a&gt; covers scheduling, webhooks, and the async job queue for production capture pipelines. For capturing pages that extend beyond the viewport, see the &lt;a href="https://www.grabbit.live/blog/full-page-screenshot" rel="noopener noreferrer"&gt;full-page screenshot guide&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/screenshot-from-url" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Puppeteer vs Playwright for Screenshots: Which Should You Use?</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Fri, 26 Jun 2026 11:37:29 +0000</pubDate>
      <link>https://dev.to/grabbit/puppeteer-vs-playwright-for-screenshots-which-should-you-use-1o92</link>
      <guid>https://dev.to/grabbit/puppeteer-vs-playwright-for-screenshots-which-should-you-use-1o92</guid>
      <description>&lt;p&gt;If you only need to take a screenshot, Puppeteer and Playwright are closer than the endless "which is better" debates suggest. Both control a headless browser, both capture the viewport, a full page, or a single element, and the code looks almost the same in either one. The real differences show up at the edges: how many browser engines you need, how much waiting code you want to write, and whether you should be running a browser at all. This guide compares the two specifically for screenshots, not general test automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chromium-only, smallest footprint:&lt;/strong&gt; Puppeteer. It is built around Chrome and the Chrome DevTools Protocol, so it does less and installs lighter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-browser screenshots (including Safari/WebKit):&lt;/strong&gt; Playwright. One API drives Chromium, Firefox, and WebKit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want fewer flaky captures with less timing code:&lt;/strong&gt; Playwright. Its auto-waiting fires actions after the page settles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You do not want to run a browser at all:&lt;/strong&gt; neither. A &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;hosted screenshot API&lt;/a&gt; takes one request and returns an image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of this post shows the actual code so you can see how small the gap is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same screenshot in both
&lt;/h2&gt;

&lt;p&gt;Here is a viewport screenshot in Puppeteer:&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;import&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="k"&gt;from&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;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://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;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;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;p&gt;And the equivalent in Playwright:&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;import&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="k"&gt;from&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;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://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;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;playwright.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;p&gt;The one meaningful difference is hiding in the &lt;code&gt;goto&lt;/code&gt; call. In Puppeteer you reach for &lt;code&gt;waitUntil: 'networkidle0'&lt;/code&gt; so you do not capture before images and fonts finish loading. Playwright auto-waits for the page to reach a stable state before the screenshot fires, so you write less timing code to get a clean capture. That auto-waiting is Playwright's biggest day-to-day ergonomic win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full-page screenshots
&lt;/h2&gt;

&lt;p&gt;Both stitch the entire scrolling page into one tall image. The flags are nearly identical.&lt;/p&gt;

&lt;p&gt;Puppeteer:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same caveats apply to both, because they come from the page, not the library. Lazy-loaded sections that only render on scroll can come back missing, and &lt;code&gt;position: fixed&lt;/code&gt; headers can repeat or float over the stitched image. The fix in either tool is to scroll the page to the bottom yourself before capturing so every section renders. See &lt;a href="https://www.grabbit.live/blog/full-page-screenshot" rel="noopener noreferrer"&gt;how to take a full-page screenshot&lt;/a&gt; for the scroll-to-bottom pattern that works in both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Single-element screenshots
&lt;/h2&gt;

&lt;p&gt;To capture one component, both libraries crop to an element's bounding box.&lt;/p&gt;

&lt;p&gt;Puppeteer gets an &lt;code&gt;ElementHandle&lt;/code&gt; and calls &lt;code&gt;screenshot()&lt;/code&gt; on it:&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;card&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;#pricing-card&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;card&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;card.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;Playwright uses a locator, which auto-waits for the element to be ready:&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;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#pricing-card&lt;/span&gt;&lt;span class="dl"&gt;'&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;card.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 where auto-waiting earns its keep. In Puppeteer, &lt;code&gt;page.$()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; if the element has not rendered yet, so you add &lt;code&gt;await page.waitForSelector('#pricing-card')&lt;/code&gt; first. Playwright's locator waits for you, so there is less to race.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-browser: Playwright's real advantage
&lt;/h2&gt;

&lt;p&gt;This is the one feature that is not a tie. Puppeteer is built for Chromium and Chrome (its Firefox support is experimental). Playwright drives Chromium, Firefox, and WebKit with the same API, which means you can capture the same page in the engine behind Safari without a Mac in the loop:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;webkit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&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;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;webkit&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://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;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;safari-engine.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;p&gt;If your screenshots need to reflect how a page actually renders in Safari (different font rendering, different layout quirks), Playwright is the answer and Puppeteer is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed and memory
&lt;/h2&gt;

&lt;p&gt;For a single screenshot, the library is rarely the bottleneck. Most of the wall-clock time is the page loading, not Puppeteer or Playwright. On simple Chrome-only scripts Puppeteer can be marginally faster because it does less setup, and its memory footprint is a touch smaller. Playwright's auto-waiting trades a little raw speed for more consistent captures under load, which tends to matter more once screenshots run in CI or production. If you are choosing on speed alone, you are probably optimizing the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where both tools stop being worth it
&lt;/h2&gt;

&lt;p&gt;Everything above runs fine on your laptop. The cost shows up in production, and it is identical for Puppeteer and Playwright because both ship the same headless browser problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provisioning.&lt;/strong&gt; Headless Chromium (and Firefox, and WebKit) need a long list of system libraries. Slim containers and serverless functions hit missing-dependency errors before the first pixel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory and zombie processes.&lt;/strong&gt; A browser not closed on every error path leaks memory and orphans processes until the box falls over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patching.&lt;/strong&gt; Browser engines ship security updates constantly. A long-lived screenshot service is now a browser fleet you have to keep current.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency.&lt;/strong&gt; One browser does one job at a time well. Ten thousand captures a day means a pool, a queue, and back-pressure to build and operate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is screenshot code. It is infrastructure, and it is the same whether you picked Puppeteer or Playwright.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same capture as one API call
&lt;/h2&gt;

&lt;p&gt;When screenshots are a production feature rather than a step inside an existing test or scraping job, a hosted &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt; skips the browser entirely. Here is the full-page capture above as a single request to Grabbit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.grabbit.live/v1/grabs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com",
    "width": 1280,
    "full_page": true,
    "format": "webp"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes a hosted &lt;code&gt;image_url&lt;/code&gt; you can use directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grb_01jx..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.grabbit.live/grabs/grb_01jx....webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1180&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The options you reach for in both libraries map onto request parameters: &lt;code&gt;fullPage&lt;/code&gt; becomes &lt;code&gt;full_page&lt;/code&gt;, the element handle or locator becomes a &lt;code&gt;selector&lt;/code&gt; field, and the manual wait becomes &lt;code&gt;delay_ms&lt;/code&gt; (0 to 10000). Width accepts 320 to 1920, height 240 to 1080, and &lt;code&gt;format&lt;/code&gt; is &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;jpeg&lt;/code&gt;, or &lt;code&gt;webp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Already running Puppeteer for Chrome work?&lt;/strong&gt; Keep it. Adding a screenshot is one line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need cross-browser captures, or want less timing code?&lt;/strong&gt; Playwright.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots are a feature you ship, not a side effect of a test?&lt;/strong&gt; Skip the browser and call an API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the framework-specific deep dives, see &lt;a href="https://www.grabbit.live/blog/puppeteer-screenshot" rel="noopener noreferrer"&gt;taking screenshots in Puppeteer&lt;/a&gt; and &lt;a href="https://www.grabbit.live/blog/playwright-screenshot" rel="noopener noreferrer"&gt;taking screenshots in Playwright&lt;/a&gt;. If you are weighing hosted options, the &lt;a href="https://www.grabbit.live/blog/best-screenshot-api" rel="noopener noreferrer"&gt;honest comparison of screenshot APIs&lt;/a&gt; covers the trade-offs without the marketing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/puppeteer-vs-playwright" rel="noopener noreferrer"&gt;Grabbit blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>node</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
