<?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.clauneck.workers.dev/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.clauneck.workers.dev/grabbit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.clauneck.workers.dev/feed/grabbit"/>
    <language>en</language>
    <item>
      <title>The Best Visual Regression Testing Tools (Open Source and SaaS)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Thu, 25 Jun 2026 11:37:10 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/the-best-visual-regression-testing-tools-open-source-and-saas-55fl</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/the-best-visual-regression-testing-tools-open-source-and-saas-55fl</guid>
      <description>&lt;p&gt;If you search for visual regression testing tools you get a wall of "top 10" lists that all name slightly different products. This is a shorter, opinionated map: the open-source options worth starting with, the SaaS platforms worth paying for, and the one thing every tool depends on but few of the lists mention, a consistent capture.&lt;/p&gt;

&lt;p&gt;For a primer on the workflow itself, see &lt;a href="https://www.grabbit.live/blog/visual-regression-testing" rel="noopener noreferrer"&gt;visual regression testing: a practical guide&lt;/a&gt;. This post is about choosing a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the tools differ (the three questions that matter)
&lt;/h2&gt;

&lt;p&gt;Every visual regression tool runs the same capture, diff, review, approve loop. They differ on three axes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What it captures:&lt;/strong&gt; isolated components, a local dev server, or deployed URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where it runs:&lt;/strong&gt; your CI (open source) or a hosted service (SaaS).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How you review diffs:&lt;/strong&gt; a generated HTML report, or a managed dashboard with baseline approval.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Match those to how you already build and the choice gets easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Playwright&lt;/strong&gt; ships &lt;code&gt;expect(page).toHaveScreenshot()&lt;/code&gt;, which captures and diffs against a stored baseline in one assertion. If you already run Playwright for end-to-end tests, this is the lowest-friction start. See &lt;a href="https://www.grabbit.live/blog/playwright-screenshot" rel="noopener noreferrer"&gt;how to take screenshots in Playwright&lt;/a&gt; for the capture options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BackstopJS&lt;/strong&gt; is the long-standing choice for page-level scenarios. You define viewports and URLs in a config file, and it generates a clean diff report. Good when you are testing pages rather than components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argos&lt;/strong&gt; focuses on the review experience: it ingests screenshots from your CI and gives you a GitHub-integrated diff UI, with a self-hostable option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loki&lt;/strong&gt; is built for Storybook, capturing each story as a component snapshot. A natural fit if your design system already lives in Storybook.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These cover most needs without a subscription. The trade-off is that you own the infrastructure: running the browsers, storing baselines, and wiring the report into CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  SaaS platforms
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chromatic&lt;/strong&gt; is the managed companion to Storybook, made by the Storybook team. It renders every story across browsers and gives you a review-and-approve workflow. Strong for component-driven teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Percy&lt;/strong&gt; (part of BrowserStack) snapshots responsive pages and integrates tightly with CI, with a hosted dashboard for baseline management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applitools&lt;/strong&gt; leads on comparison technology: its Visual AI aims to flag meaningful changes while ignoring noise like anti-aliasing, which reduces false positives at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sauce Labs&lt;/strong&gt; bundles visual testing into a broader cross-browser cloud, useful if you already run functional tests there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You pay for the review dashboard, cross-browser rendering, and not having to maintain capture infrastructure yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part every tool depends on: a consistent capture
&lt;/h2&gt;

&lt;p&gt;A visual diff is only as trustworthy as the screenshot under it. The reason teams mute their visual suites is flakiness, and flakiness almost always comes from the capture step, not the diff: animations caught mid-flight, lazy-loaded content that had not settled, fonts loaded a frame late, or a viewport that drifted between baseline and comparison.&lt;/p&gt;

&lt;p&gt;Component-bound tools control this by rendering in a sandbox. But when you want to test &lt;strong&gt;deployed URLs&lt;/strong&gt; (staging, preview deploys, production canaries), you need a capture that looks identical every run, given the same input, without standing up and scaling headless browsers in CI. A screenshot API gives you exactly that:&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://staging.example.com/pricing",
    "width": 1280,
    "full_page": true,
    "delay_ms": 500
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pinning &lt;code&gt;width&lt;/code&gt; keeps the layout identical between runs, &lt;code&gt;full_page&lt;/code&gt; captures the whole document, and &lt;code&gt;delay_ms&lt;/code&gt; waits for content to settle so lazy-loaded sections do not register as false diffs. Store the returned &lt;code&gt;image_url&lt;/code&gt; as your baseline, capture the same URL on each deploy, and feed both images into whichever diff tool you picked above. Grabbit does not diff images for you; it is the deterministic capture layer your chosen tool builds on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Already using Playwright:&lt;/strong&gt; add one &lt;code&gt;toHaveScreenshot&lt;/code&gt; assertion and grow from there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component library in Storybook:&lt;/strong&gt; Loki (open source) or Chromatic (managed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing deployed pages, want it free:&lt;/strong&gt; BackstopJS or Argos, fed consistent captures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want a managed dashboard and minimal upkeep:&lt;/strong&gt; Percy or Applitools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever you choose, start with a handful of high-value screens, make the captures deterministic first, and only expand once the suite is quiet enough to trust. For running captures on a schedule or across many URLs, see &lt;a href="https://www.grabbit.live/automated-screenshots" rel="noopener noreferrer"&gt;automated screenshots&lt;/a&gt;, and for every capture option see the &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;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/visual-regression-testing-tools" 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 Playwright (Full Page, Elements, CI)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Wed, 24 Jun 2026 11:37:41 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/how-to-take-screenshots-in-playwright-full-page-elements-ci-11gc</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/how-to-take-screenshots-in-playwright-full-page-elements-ci-11gc</guid>
      <description>&lt;p&gt;&lt;code&gt;page.screenshot()&lt;/code&gt; does the job in two lines for a local capture or a test assertion. The friction arrives later: full-page shots that get clipped by sticky headers, Chromium installation issues in CI, and the long tail of keeping a browser fleet running in production. This guide covers the screenshot methods that work, the ones with hidden edge cases, and the point where offloading to an API makes more sense.&lt;/p&gt;

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

&lt;p&gt;Launch a browser, open a page, capture:&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;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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;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="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 &lt;code&gt;waitUntil: 'networkidle'&lt;/code&gt; option is worth stating explicitly. The default &lt;code&gt;load&lt;/code&gt; event fires as soon as the HTML and blocking scripts are done, but client-rendered content, web fonts, and lazy-loaded images often arrive later. &lt;code&gt;networkidle&lt;/code&gt; waits until there are no network connections open for at least 500ms, which is a much better proxy for "the page actually looks right."&lt;/p&gt;

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

&lt;p&gt;By default &lt;code&gt;page.screenshot()&lt;/code&gt; captures the viewport only. To capture the full scrolling page, set &lt;code&gt;fullPage: true&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="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 expands the viewport to the document's full scroll height before capturing. This works reliably on most static pages.&lt;/p&gt;

&lt;p&gt;Two things break it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sticky and fixed-position elements.&lt;/strong&gt; A header with &lt;code&gt;position: fixed&lt;/code&gt; renders in its normal viewport position for every "slice" Playwright captures, so it can appear repeated or floating over content in the final stitched image. There is no clean built-in workaround short of hiding the element with a &lt;code&gt;page.addStyleTag&lt;/code&gt; before capturing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy-loaded content.&lt;/strong&gt; If a page only renders sections when they scroll into view, &lt;code&gt;fullPage: true&lt;/code&gt; can come back short. Scroll the page yourself before capturing to trigger those renders:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&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="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;scrolled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&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;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;scrolled&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrolled&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;resolve&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="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;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;h2&gt;
  
  
  Screenshotting a specific element
&lt;/h2&gt;

&lt;p&gt;Playwright's locator API makes element-level captures cleaner than Puppeteer's &lt;code&gt;ElementHandle&lt;/code&gt; approach. Call &lt;code&gt;.screenshot()&lt;/code&gt; on any locator and Playwright crops to that element's bounding box:&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="c1"&gt;// Capture a single component by CSS selector&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;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;span class="c1"&gt;// Or by role&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;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dialog&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;modal.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;If the element is below the fold, Playwright scrolls it into view before capturing. If the selector matches nothing, the call throws, so wrap it in a &lt;code&gt;waitFor&lt;/code&gt; when the element might not be immediately present:&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;card&lt;/span&gt; &lt;span class="o"&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;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="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;waitFor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visible&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;h2&gt;
  
  
  Format and quality options
&lt;/h2&gt;

&lt;p&gt;Playwright defaults to PNG. For production use cases where file size matters, switch to JPEG or WebP:&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;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;page.webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;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;webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 0–100, only for jpeg and webp&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PNG is lossless and best for visual regression tests where pixel-level accuracy matters. JPEG and WebP are better for images that will be served over HTTP, since they are significantly smaller at comparable quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots in CI with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Installing Playwright in CI requires browser binaries and their system dependencies. Use &lt;code&gt;npx playwright install --with-deps&lt;/code&gt; (not just &lt;code&gt;npx playwright install&lt;/code&gt;) to pull in the OS libraries Chromium needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Playwright browsers&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright install --with-deps chromium&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload test artifacts&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playwright-report&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test-results/&lt;/span&gt;
    &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;if: always()&lt;/code&gt; on the upload step is important: test artifacts are most useful when tests fail, and a plain &lt;code&gt;if: success()&lt;/code&gt; would skip the upload precisely when you need the diffs.&lt;/p&gt;

&lt;p&gt;Playwright's &lt;code&gt;--with-deps&lt;/code&gt; flag is the most common CI pitfall. Without it, Chromium launches fine locally (because those libraries are already installed) but fails in a minimal CI container with a cryptic &lt;code&gt;error while loading shared libraries&lt;/code&gt; message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual regression with screenshots
&lt;/h2&gt;

&lt;p&gt;Playwright's built-in snapshot assertion compares a screenshot against a stored baseline:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, Playwright writes the baseline to &lt;code&gt;__snapshots__/&lt;/code&gt;. On subsequent runs it compares pixel-by-pixel and fails if the diff exceeds the configured threshold. Pass &lt;code&gt;{ maxDiffPixelRatio: 0.01 }&lt;/code&gt; to tolerate minor anti-aliasing differences:&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;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;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;.hero-section&lt;/span&gt;&lt;span class="dl"&gt;'&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;hero.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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a longer look at visual regression workflows, the &lt;a href="https://www.grabbit.live/blog/visual-regression-testing" rel="noopener noreferrer"&gt;visual regression testing guide&lt;/a&gt; covers when snapshot tests make sense and how to keep them from becoming brittle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where running Playwright yourself gets costly
&lt;/h2&gt;

&lt;p&gt;Playwright is the right tool for test suites that already drive a browser. When screenshots are a standalone production feature, the overhead adds up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chromium in production.&lt;/strong&gt; Deploying a headless browser to a serverless function means bundling system libraries, increasing cold-start times, and hitting the payload size limits of most platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency.&lt;/strong&gt; One browser instance handles one capture at a time well. Handling bursts means a pool, a queue, and back-pressure logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security surface.&lt;/strong&gt; A browser that can render arbitrary user-submitted URLs needs SSRF protection (block private IP ranges), sandboxing, and regular security patches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the point where a dedicated &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt; is faster to operate: the browser fleet, SSRF guards, and scaling are someone else's problem.&lt;/p&gt;

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

&lt;p&gt;Here is the full-page capture above as a single request to Grabbit. No browser to provision or patch:&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,
    "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;52340&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;The Playwright options you reach for most translate directly: &lt;code&gt;fullPage: true&lt;/code&gt; becomes &lt;code&gt;"full_page": true&lt;/code&gt;, the element locator pattern becomes a &lt;code&gt;"selector"&lt;/code&gt; field, and manual wait time 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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# capture one component after waiting for it to settle&lt;/span&gt;
&lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "url": "https://example.com/dashboard",
  "selector": "#chart-container",
  "delay_ms": 800,
  "format": "png",
  "width": 1280,
  "height": 720
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Use Playwright directly when screenshots are part of a test suite that already controls a browser, or when you need precise assertions against local builds that are not reachable from the internet. Use an API when screenshots are a production feature and you would rather ship in an afternoon than build a browser fleet.&lt;/p&gt;

&lt;p&gt;For a head-to-head look at Puppeteer's API versus Playwright's for captures, see the &lt;a href="https://www.grabbit.live/blog/puppeteer-screenshot" rel="noopener noreferrer"&gt;Puppeteer screenshot guide&lt;/a&gt;. If you are picking a hosted screenshot service, the &lt;a href="https://www.grabbit.live/blog/best-screenshot-api" rel="noopener noreferrer"&gt;screenshot API comparison&lt;/a&gt; covers the trade-offs honestly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/playwright-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 Test and Preview Your OG Image Before Publishing</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Tue, 23 Jun 2026 11:39:33 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/how-to-test-and-preview-your-og-image-before-publishing-mck</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/how-to-test-and-preview-your-og-image-before-publishing-mck</guid>
      <description>&lt;p&gt;You added an &lt;code&gt;og:image&lt;/code&gt; tag, pushed your changes, shared the link, and the preview is wrong: the old image, a cropped logo, or nothing at all. The fix is to preview the card &lt;em&gt;before&lt;/em&gt; you publish, not after. This guide covers the fastest ways to test an OG image, including the case the online tools cannot handle: a page running on localhost.&lt;/p&gt;

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

&lt;p&gt;Run your page URL through a free Open Graph checker. The three that fetch your live page and render the card the way each platform would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.opengraph.xyz/" rel="noopener noreferrer"&gt;opengraph.xyz&lt;/a&gt;&lt;/strong&gt; scans any URL and previews the card across Facebook, X, LinkedIn, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://orcascan.com/tools/open-graph-validator" rel="noopener noreferrer"&gt;Open Graph Validator&lt;/a&gt; (Orca Scan)&lt;/strong&gt; lists every tag it found and flags what is missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.debugbear.com/open-graph-checker" rel="noopener noreferrer"&gt;DebugBear's OG checker&lt;/a&gt;&lt;/strong&gt; validates the tags and the image dimensions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paste your URL, read the rendered card, fix any tag it flags, redeploy. That covers a page that is already live. The harder cases are below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the platform debuggers for the real result
&lt;/h2&gt;

&lt;p&gt;The general checkers above are fast, but the platforms cache what &lt;em&gt;they&lt;/em&gt; scrape, and that cache is what users actually see. To test against the real thing and to clear a stale preview, go to the source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Facebook:&lt;/strong&gt; &lt;a href="https://developers.facebook.com/tools/debug/" rel="noopener noreferrer"&gt;Sharing Debugger&lt;/a&gt;. Shows exactly what the crawler sees and has a &lt;strong&gt;Scrape Again&lt;/strong&gt; button to force a fresh fetch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/post-inspector/" rel="noopener noreferrer"&gt;Post Inspector&lt;/a&gt;. Re-fetches on every check, so it doubles as a cache buster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X (Twitter):&lt;/strong&gt; X reads &lt;code&gt;og:image&lt;/code&gt;, but the large card only renders when &lt;code&gt;twitter:card&lt;/code&gt; is set to &lt;code&gt;summary_large_image&lt;/code&gt;. The simplest live test is to paste the URL into a draft post and look at the attached card before sending.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you changed your image and the old one still appears, this is almost always a cache issue. Re-scrape through Facebook's debugger or LinkedIn's inspector and it clears within minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to test an OG image on localhost
&lt;/h2&gt;

&lt;p&gt;This is the question the online tools cannot answer. A social debugger runs on the platform's servers, so it has no route to &lt;code&gt;http://localhost:3000&lt;/code&gt;. You have two options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: expose localhost with a tunnel.&lt;/strong&gt; Run a tunnel like ngrok to get a public URL that points at your local server, then paste that URL into any of the checkers above. This gives you the real unfurl preview while your code is still local.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: capture the OG template directly.&lt;/strong&gt; If your OG images are generated from an HTML template (the scalable pattern, where one &lt;code&gt;/og&lt;/code&gt; route renders the title and branding), you do not need a social crawler to see the &lt;em&gt;image&lt;/em&gt;. You need to see what the template renders at 1200 by 630. Point a screenshot API at the template URL and inspect the returned image:&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_test_..."&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://your-tunnel.ngrok.app/og?title=My+Post+Title&amp;amp;category=Guides",
    "width": 1200,
    "height": 630,
    "format": "png"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response returns a hosted &lt;code&gt;image_url&lt;/code&gt; showing exactly what your template produces at the OG card size. You are checking the rendered pixels (Is the title cut off? Does the logo fit? Is the contrast readable?) rather than the social unfurl. Use a &lt;code&gt;sk_test_&lt;/code&gt; key while you iterate; test grabs return a deterministic placeholder and never cost a credit, so you can capture as often as you like while tuning the template.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pre-publish checklist
&lt;/h2&gt;

&lt;p&gt;Before you ship a page, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;og:image&lt;/code&gt; points to an &lt;strong&gt;absolute&lt;/strong&gt; URL (including &lt;code&gt;https://&lt;/code&gt;), not a relative path. Crawlers do not resolve relative paths.&lt;/li&gt;
&lt;li&gt;The image is at least 600 by 315, ideally &lt;strong&gt;1200 by 630&lt;/strong&gt;. Below the minimum, the card collapses to a thumbnail.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;og:image:width&lt;/code&gt; and &lt;code&gt;og:image:height&lt;/code&gt; are set so the platform reserves the right space.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;twitter:card&lt;/code&gt; is set to &lt;code&gt;summary_large_image&lt;/code&gt; if you want the large card on X.&lt;/li&gt;
&lt;li&gt;The image URL is publicly reachable, with no auth wall or redirect in front of it. Some crawlers do not follow redirects.&lt;/li&gt;
&lt;li&gt;Your meta tags are server-rendered, not injected by client JavaScript. Crawlers run little to no JS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all six pass and the debugger still shows the wrong image, force a re-scrape to clear the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why previewing matters more for dynamic images
&lt;/h2&gt;

&lt;p&gt;A single hand-made OG image is easy to eyeball once. But the moment you generate images dynamically (one per blog post, one per product, one per user) you cannot manually check every output. That is exactly when previewing through a screenshot API pays off: the same call that &lt;em&gt;generates&lt;/em&gt; the image is the call that lets you &lt;em&gt;inspect&lt;/em&gt; it, so you can catch a clipped title or an overflowing string before any of them go live.&lt;/p&gt;

&lt;p&gt;This is the workflow &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;Grabbit's screenshot API&lt;/a&gt; is built for. Render your OG template, capture it at 1200 by 630, store the returned &lt;code&gt;image_url&lt;/code&gt;, and drop it into your &lt;code&gt;og:image&lt;/code&gt; tag. Start with a free test key to dial in the template, then switch to a live key for production captures.&lt;/p&gt;

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

&lt;p&gt;To preview an OG image before publishing: run the live URL through opengraph.xyz or a platform debugger, force a re-scrape if a stale image is cached, and for localhost either tunnel the page out or capture your OG template directly with a screenshot API. For the tags themselves see &lt;a href="https://www.grabbit.live/blog/what-is-an-og-image" rel="noopener noreferrer"&gt;what an OG image is and why links break&lt;/a&gt;, and for the exact pixel sizes see &lt;a href="https://www.grabbit.live/blog/og-image-sizes" rel="noopener noreferrer"&gt;Open Graph image sizes and dimensions&lt;/a&gt;. To automate the generation step, read &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;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/og-image-checker" 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>What Is an OG Image? Why Your Shared Links Look Broken</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:08:43 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/what-is-an-og-image-why-your-shared-links-look-broken-pnd</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/what-is-an-og-image-why-your-shared-links-look-broken-pnd</guid>
      <description>&lt;p&gt;When someone shares your URL on Slack, X, or LinkedIn, the platform fetches a small metadata snapshot of your page and renders a preview card. The image in that card is the OG image. If you have not explicitly set one, the platform either guesses (often badly) or shows nothing at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "OG" stands for
&lt;/h2&gt;

&lt;p&gt;OG is short for Open Graph, a protocol Facebook introduced in 2010 to give websites a standard way to describe themselves to social crawlers. The spec defines a set of &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags you put in your page's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. The most important ones are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:title"&lt;/span&gt;       &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Page title"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"One-sentence description."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt;       &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://yoursite.com/og/home.png"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image:width"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image:height"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:url"&lt;/span&gt;         &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://yoursite.com/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every major platform reads these tags: Facebook, LinkedIn, X, Slack, Discord, iMessage, WhatsApp, and Teams. The &lt;code&gt;og:image&lt;/code&gt; tag is the one that fills the visual slot in the preview card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why shared links look broken without one
&lt;/h2&gt;

&lt;p&gt;When a platform crawler visits your URL and finds no &lt;code&gt;og:image&lt;/code&gt; tag, it either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Picks the first image it finds on the page (often a logo, icon, or product thumbnail that was not designed for 1200 by 630)&lt;/li&gt;
&lt;li&gt;Falls back to a gray placeholder&lt;/li&gt;
&lt;li&gt;Shows nothing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those outcomes help. A broken preview costs clicks because users cannot tell at a glance what the link is about. A well-set OG image does the opposite: it communicates the content before anyone reads the title.&lt;/p&gt;

&lt;h2&gt;
  
  
  The standard dimensions
&lt;/h2&gt;

&lt;p&gt;The universally safe size is &lt;strong&gt;1200 by 630 pixels&lt;/strong&gt; (a 1.91:1 ratio). This is what Facebook, LinkedIn, X, Slack, and Discord all render as a full-width card. Below 600 by 315 most platforms shrink the image to a small thumbnail beside the title instead of a full card.&lt;/p&gt;

&lt;p&gt;Set the width and height tags alongside the image URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt;        &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://yoursite.com/og/post-slug.webp"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image:width"&lt;/span&gt;  &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image:height"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For exact breakdowns by platform see &lt;a href="https://www.grabbit.live/blog/og-image-sizes" rel="noopener noreferrer"&gt;Open Graph image sizes and dimensions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  X (Twitter) cards
&lt;/h2&gt;

&lt;p&gt;X reads &lt;code&gt;og:image&lt;/code&gt; automatically, but to trigger the large preview format you need one extra tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"twitter:card"&lt;/span&gt;  &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"summary_large_image"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"twitter:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://yoursite.com/og/post-slug.webp"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;twitter:card&lt;/code&gt; set to &lt;code&gt;summary_large_image&lt;/code&gt;, X collapses your preview to a small thumbnail regardless of your &lt;code&gt;og:image&lt;/code&gt; dimensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one image per page beats one image for everything
&lt;/h2&gt;

&lt;p&gt;A global fallback image (like your homepage hero) keeps every shared link looking identical. A per-page OG image tells the reader at a glance what that specific page is about. Blog posts, product pages, and documentation pages all carry different information, and the preview card is the first signal users see in a feed.&lt;/p&gt;

&lt;p&gt;The friction here is design. Creating a unique image for every page does not scale unless you automate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating OG images with a screenshot API
&lt;/h2&gt;

&lt;p&gt;The scalable approach: build one HTML template that renders your brand, title, and description at 1200 by 630, then capture it with a screenshot API once per page. The template lives in your codebase; the API handles the headless browser.&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/og?title=What+Is+an+OG+Image&amp;amp;category=Guides",
    "width": 1200,
    "height": 630,
    "format": "webp"
  }'&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; you can use directly in 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....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;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;"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;41280&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;870&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;Call the API once at publish time or on the first request, store the returned URL, and serve it on every subsequent visit. One API call per unique page; no design tool required.&lt;/p&gt;

&lt;p&gt;For a step-by-step walkthrough of the template-and-capture pattern 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 full API details 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;h2&gt;
  
  
  Checking what your OG image actually looks like
&lt;/h2&gt;

&lt;p&gt;After adding the tags, verify the result before publishing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Facebook:&lt;/strong&gt; &lt;a href="https://developers.facebook.com/tools/debug/" rel="noopener noreferrer"&gt;Sharing Debugger&lt;/a&gt; shows exactly what the crawler sees and lets you force a re-scrape to clear stale cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/post-inspector/" rel="noopener noreferrer"&gt;Post Inspector&lt;/a&gt; works the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X:&lt;/strong&gt; The X Card Validator previews the card layout, including whether &lt;code&gt;summary_large_image&lt;/code&gt; is triggering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Crawlers cache aggressively. If you update your &lt;code&gt;og:image&lt;/code&gt; tag and the old image still shows up, hit the platform's debugger to force a fresh fetch. For the full preview workflow, including how to test from localhost, see &lt;a href="https://www.grabbit.live/blog/og-image-checker" rel="noopener noreferrer"&gt;how to test and preview your OG image before publishing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common reasons the tag is set but the image still does not appear
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The image URL returns a redirect instead of the image directly; some crawlers do not follow redirects&lt;/li&gt;
&lt;li&gt;The image is behind authentication or a firewall the crawler cannot reach&lt;/li&gt;
&lt;li&gt;The image is smaller than 200 by 200 pixels (Facebook's minimum for displaying a full-width card)&lt;/li&gt;
&lt;li&gt;The page renders the &lt;code&gt;og:image&lt;/code&gt; tag via JavaScript after load; crawlers typically execute little to no JavaScript, so server-render your meta tags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Server-side rendering or static generation of meta tags is the reliable approach. If your framework generates &lt;code&gt;og:image&lt;/code&gt; at request time on the server, the crawler will always see it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/what-is-an-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 Capture a Full-Page Screenshot in Google Chrome</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Sun, 21 Jun 2026 12:06:25 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/how-to-capture-a-full-page-screenshot-in-google-chrome-2p4a</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/how-to-capture-a-full-page-screenshot-in-google-chrome-2p4a</guid>
      <description>&lt;p&gt;The fastest way to capture a full-page screenshot in Chrome needs no extension at all: open DevTools, run one command, and Chrome stitches the entire scrolling page into a single image. This guide covers that built-in method, the extension route if you want a one-click button, and how to automate full-page captures once you need them for more than one page at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The built-in DevTools method (no extension)
&lt;/h2&gt;

&lt;p&gt;Chrome ships a full-page capture inside Developer Tools. It is the method most "scrolling screenshot" guides eventually point you to, and it works on every desktop platform:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the page you want to capture.&lt;/li&gt;
&lt;li&gt;Open DevTools: &lt;strong&gt;F12&lt;/strong&gt;, or &lt;strong&gt;Ctrl+Shift+I&lt;/strong&gt; on Windows and Linux, or &lt;strong&gt;Cmd+Option+I&lt;/strong&gt; on Mac.&lt;/li&gt;
&lt;li&gt;Open the Command Menu: &lt;strong&gt;Ctrl+Shift+P&lt;/strong&gt; on Windows and Linux, or &lt;strong&gt;Cmd+Shift+P&lt;/strong&gt; on Mac.&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;screenshot&lt;/code&gt;, then select &lt;strong&gt;Capture full size screenshot&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Chrome scrolls the whole page, stitches it into one tall PNG, and downloads it to your default folder. This captures the entire document height, not just the part visible in the window, which is the difference between this and a normal screenshot.&lt;/p&gt;

&lt;p&gt;If you only want the visible area, the same menu offers &lt;strong&gt;Capture screenshot&lt;/strong&gt;, and &lt;strong&gt;Capture node screenshot&lt;/strong&gt; grabs a single selected element from the Elements panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The extension route
&lt;/h2&gt;

&lt;p&gt;If you would rather click a button than open DevTools each time, a full-page screenshot extension adds one to the toolbar. GoFullPage is the most widely used, and ScreenshotOne and similar tools offer browser extensions too. The trade-off: an extension can see the pages you capture, so for sensitive or internal pages the built-in DevTools method keeps everything local.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save as PDF instead
&lt;/h2&gt;

&lt;p&gt;If you need a document rather than a PNG, Chrome's print dialog does full-page export:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press &lt;strong&gt;Ctrl+P&lt;/strong&gt; (Windows/Linux) or &lt;strong&gt;Cmd+P&lt;/strong&gt; (Mac).&lt;/li&gt;
&lt;li&gt;Set the destination to &lt;strong&gt;Save as PDF&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This captures the full page as vector text where possible, which is better than an image if the page is mostly type.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the manual method stops scaling
&lt;/h2&gt;

&lt;p&gt;Everything above is one page at a time, by hand. That is fine for a handful of captures. It falls apart the moment you need full-page screenshots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;across dozens or hundreds of URLs (a sitemap audit, competitor research),&lt;/li&gt;
&lt;li&gt;on a schedule (visual monitoring, change detection),&lt;/li&gt;
&lt;li&gt;inside CI (visual regression tests on every deploy),&lt;/li&gt;
&lt;li&gt;or generated on demand (social preview images per page).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For any of those, you want an API call instead of a person and a keyboard. A &lt;a href="https://www.grabbit.live/guides/full-page-screenshot" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt; runs a real Chromium render on its side and returns a hosted image, so you script the capture instead of performing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating full-page captures
&lt;/h2&gt;

&lt;p&gt;Here is the same full-page capture as a single request to Grabbit. Set &lt;code&gt;full_page&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; and you get the entire scrolling page back, exactly like the DevTools command:&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 store or display 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;47120&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;To capture many pages, loop over your list of URLs and send one request each. Width accepts 320 to 1920, &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;, and &lt;code&gt;delay_ms&lt;/code&gt; (0 to 10000) waits for late-rendering content before the shot fires. If a page lazy-loads sections, &lt;code&gt;delay_ms&lt;/code&gt; plays the same role as scrolling the page yourself in DevTools.&lt;/p&gt;

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

&lt;p&gt;Reach for the &lt;strong&gt;DevTools Command Menu&lt;/strong&gt; for a quick one-off capture: it is built in, local, and needs nothing installed. Reach for an &lt;strong&gt;extension&lt;/strong&gt; if you want a permanent toolbar button and the pages are not sensitive. Reach for an &lt;strong&gt;API&lt;/strong&gt; when capturing is a repeated or automated job and doing it by hand no longer makes sense.&lt;/p&gt;

&lt;p&gt;For the cross-tool breakdown of full-page capture (including Firefox, Edge, and code approaches), 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;. If you are already scripting captures in code, &lt;a href="https://www.grabbit.live/blog/puppeteer-screenshot" rel="noopener noreferrer"&gt;full-page screenshots in Puppeteer&lt;/a&gt; covers the same job in Node.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/capture-full-page-screenshot-chrome" 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>Full-Page Screenshots in Puppeteer (and When an API Is Faster)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Sat, 20 Jun 2026 12:08:42 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/full-page-screenshots-in-puppeteer-and-when-an-api-is-faster-5eoj</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/full-page-screenshots-in-puppeteer-and-when-an-api-is-faster-5eoj</guid>
      <description>&lt;p&gt;&lt;code&gt;page.screenshot()&lt;/code&gt; is the whole API, and for a one-off capture on your laptop it is genuinely two lines of code. The work starts after that: full-page captures that come back cut off, fonts that render as fallbacks, and a headless Chromium process that eats memory in production. This guide covers the Puppeteer screenshot methods that work, the ones that quietly fail, and the point where running your own browser stops being worth it.&lt;/p&gt;

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

&lt;p&gt;Launch a browser, open a page, navigate, capture:&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="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 detail that matters here is &lt;code&gt;waitUntil: 'networkidle0'&lt;/code&gt;. The default for &lt;code&gt;goto&lt;/code&gt; resolves as soon as the &lt;code&gt;load&lt;/code&gt; event fires, which is often before images, web fonts, and client-rendered content are on the page. &lt;code&gt;networkidle0&lt;/code&gt; waits until there have been no network connections for 500ms, which is a much better proxy for "the page is actually done."&lt;/p&gt;

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

&lt;p&gt;By default &lt;code&gt;page.screenshot()&lt;/code&gt; captures only the viewport. To capture the entire scrolling page, set &lt;code&gt;fullPage: true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;Puppeteer measures the full scroll height and stitches the result into a single tall image. This works on most static pages. It breaks on two kinds of page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lazy-loaded content.&lt;/strong&gt; If a page only renders sections as they scroll into view, a full-page capture can stop short because the lower sections never rendered. The fix is to scroll the page to the bottom yourself before capturing, then scroll back to the top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sticky and fixed elements.&lt;/strong&gt; A header with &lt;code&gt;position: fixed&lt;/code&gt; can repeat down the stitched image or float over content, because the element stays pinned in the viewport while Puppeteer scrolls underneath it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the scroll-to-bottom workaround for lazy pages:&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;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&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;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;resolve&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="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;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;h2&gt;
  
  
  Screenshotting a single element
&lt;/h2&gt;

&lt;p&gt;To capture one component instead of the whole page, get an &lt;code&gt;ElementHandle&lt;/code&gt; and call &lt;code&gt;screenshot()&lt;/code&gt; on it. Puppeteer crops to that element's bounding box:&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;If &lt;code&gt;page.$()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;, the selector did not match yet. Use &lt;code&gt;await page.waitForSelector('#pricing-card')&lt;/code&gt; first so you are not racing the render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Higher-quality captures
&lt;/h2&gt;

&lt;p&gt;Two settings control how sharp the output looks:&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;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="na"&gt;deviceScaleFactor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// capture at 2x for a crisp, retina-density image&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// PNG is lossless; for JPEG you can trade size for quality&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;sharp.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&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;deviceScaleFactor: 2&lt;/code&gt; doubles the pixel density, which is the single biggest win for screenshots that will be displayed on high-resolution screens. Beyond that, the most common quality bug is not resolution at all: it is capturing before web fonts have loaded, so the screenshot shows a fallback font. Wait for fonts explicitly when it matters:&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;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where running Puppeteer yourself gets expensive
&lt;/h2&gt;

&lt;p&gt;Everything above runs fine on your machine. Production is where the cost shows up, and none of it is about the screenshot code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provisioning Chromium.&lt;/strong&gt; Headless Chromium needs a long list of system libraries. In a slim container or a serverless function you hit missing-dependency errors before you capture a single pixel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory and zombie processes.&lt;/strong&gt; A browser that is not closed on every error path leaks memory. Under load you accumulate orphaned Chromium processes until the box falls over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patching.&lt;/strong&gt; Chromium ships security updates constantly. A long-lived screenshot service is a browser you now have to keep current.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency.&lt;/strong&gt; One browser handles one job at a time well. Ten thousand captures a day means a pool, a queue, and back-pressure, which is real infrastructure to build and operate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the point where a hosted &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt; is faster to ship: someone else runs the browser fleet, and you send one HTTP request.&lt;/p&gt;

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

&lt;p&gt;Here is the full-page capture above as a single request to Grabbit. No browser to install, patch, or scale:&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 Puppeteer options you reach for most map directly onto request parameters: &lt;code&gt;fullPage&lt;/code&gt; becomes &lt;code&gt;full_page&lt;/code&gt;, the element handle pattern 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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# capture one element, after waiting 500ms for it to settle&lt;/span&gt;
&lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "url": "https://example.com/pricing",
  "selector": "#pricing-card",
  "delay_ms": 500,
  "format": "png"
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Reach for Puppeteer directly when you are already driving a browser for other reasons (scraping, end-to-end tests) and a screenshot is one more step, or when you need a one-off capture during development. Reach for an API when screenshots are a production feature and you would rather not operate a browser fleet to ship it.&lt;/p&gt;

&lt;p&gt;For the full breakdown of full-page capture techniques across tools, 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;. 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-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 Generate Dynamic OG Images from Any URL</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Fri, 19 Jun 2026 12:07:26 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/how-to-generate-dynamic-og-images-from-any-url-12c</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/how-to-generate-dynamic-og-images-from-any-url-12c</guid>
      <description>&lt;p&gt;Most sites share links with a broken card or a generic company logo. The pages that get clicks have a sharp 1200 by 630 image that shows the post title and brand. Designing that image in a tool does not scale past a handful of pages. The approach that does: one HTML template, captured by an API, cached forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a template-and-capture approach
&lt;/h2&gt;

&lt;p&gt;A dynamic OG image has the same data as the page it represents: a title, maybe a description or author. If you can render that data in HTML, a screenshot API can turn it into an image. The template lives in your codebase, the API handles the browser, and you never open a design tool.&lt;/p&gt;

&lt;p&gt;This pattern also means a single CSS change to the template updates every OG image across the site. No re-exporting, no version drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Build the template page
&lt;/h2&gt;

&lt;p&gt;Create a route at &lt;code&gt;/og&lt;/code&gt; that reads query parameters and renders a styled card at 1200 by 630. A minimal server-rendered example in any framework looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1200px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;630px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0f172a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nc"&gt;.eyebrow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#60a5fa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nc"&gt;.title&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f8fafc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;68px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;700&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nc"&gt;.logo&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;48px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#94a3b8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"eyebrow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ category }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ title }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"logo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;yoursite.com&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap the Mustache-style placeholders for whatever your server framework uses. Keep the body fixed at 1200 by 630 so the viewport matches the capture size exactly. Add your actual brand colors, a logo SVG, or a background gradient, then preview the page in a browser to confirm it looks right before automating the capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Capture the template with a screenshot API
&lt;/h2&gt;

&lt;p&gt;Once the template is live at a public URL, generating an image is one POST 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://yoursite.com/og?title=How+to+Build+a+Screenshot+API&amp;amp;category=Guides",
    "width": 1200,
    "height": 630,
    "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 drop directly 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....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;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;"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;38420&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;910&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;Use &lt;code&gt;"format": "webp"&lt;/code&gt; for smaller file sizes. If your template renders text via JavaScript instead of server-side, add &lt;code&gt;"delay_ms": 500&lt;/code&gt; to give the script time to run before the capture fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it dynamic
&lt;/h2&gt;

&lt;p&gt;The URL is the variable part. For a blog, construct the template URL per post and call the API once at publish time or on the first request:&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;generateOgImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&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="nx"&gt;category&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;templateUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;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://yoursite.com/og&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&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="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&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="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;// store this; don't call the API again on repeat visits&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the returned &lt;code&gt;image_url&lt;/code&gt; in your CMS, database, or a build-time JSON file. On every subsequent request, serve the stored URL. The API call runs once per unique image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Waiting for JavaScript-rendered content
&lt;/h2&gt;

&lt;p&gt;If your template depends on a framework that hydrates after the initial HTML is served, the screenshot may fire before the text is visible. Two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed delay:&lt;/strong&gt; &lt;code&gt;"delay_ms": 800&lt;/code&gt; waits that many milliseconds after the page loads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Element selector:&lt;/strong&gt; &lt;code&gt;"selector": "#title"&lt;/code&gt; waits until that element is present in the DOM before capturing. This is more reliable than a fixed delay because it ties the capture to a real content signal rather than a guess.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a pure server-rendered template, neither option is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching and the og:image meta tag
&lt;/h2&gt;

&lt;p&gt;Once you have the &lt;code&gt;image_url&lt;/code&gt;, wire it into your page head:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.grabbit.live/grabs/grb_01jx....webp"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image:width"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image:height"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"twitter:card"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"summary_large_image"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"twitter:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.grabbit.live/grabs/grb_01jx....webp"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;og:image:width&lt;/code&gt; and &lt;code&gt;og:image:height&lt;/code&gt; so platforms reserve the right space without fetching the image first. For the &lt;code&gt;twitter:card&lt;/code&gt; value, &lt;code&gt;summary_large_image&lt;/code&gt; is the one that renders the big preview on X.&lt;/p&gt;

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

&lt;p&gt;The full workflow: build one HTML template, pass page data as query parameters, call the &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;Grabbit screenshot API&lt;/a&gt; once, store the &lt;code&gt;image_url&lt;/code&gt;, and serve it from your &lt;code&gt;og:image&lt;/code&gt; tag. Update the template design once and every image refreshes on the next capture.&lt;/p&gt;

&lt;p&gt;For exact OG image dimensions and what each platform expects, see &lt;a href="https://www.grabbit.live/blog/og-image-sizes" rel="noopener noreferrer"&gt;Open Graph image sizes and dimensions&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/og-image-generator" 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>The Best Screenshot APIs in 2026 (An Honest Comparison)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Thu, 18 Jun 2026 07:46:45 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/the-best-screenshot-apis-in-2026-an-honest-comparison-cka</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/the-best-screenshot-apis-in-2026-an-honest-comparison-cka</guid>
      <description>&lt;p&gt;Most "best screenshot API" posts are affiliate-link roundups. Ratings correlate with payout, not with what actually fits your use case. This post uses verified pricing data (June 2026) and compares on the criteria that matter for developers: billing model, per-grab cost, features, and how well the API fits automation and agent workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for
&lt;/h2&gt;

&lt;p&gt;These are the dimensions that separate one screenshot API from another in practice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Billing model.&lt;/strong&gt; Monthly subscriptions reset unused credits every cycle. If your volume is spiky or seasonal, you pay for capacity you do not use. Annual flat plans and prepaid credits do not reset. For variable workloads, the billing model often matters more than the per-grab rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-grab cost.&lt;/strong&gt; At moderate volume, rates range from $0.0002 to $0.0079 per capture. A higher per-grab rate is not always worse: some providers include features (cookie consent, selector targeting) that others charge extra for or omit entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core features.&lt;/strong&gt; Full-page capture, custom viewport, output format (PNG/JPEG/WebP), &lt;code&gt;delay_ms&lt;/code&gt; wait controls for JavaScript-heavy pages, and CSS &lt;code&gt;selector&lt;/code&gt; clipping are the set you need for any non-trivial pipeline. Cookie-consent dismissal is increasingly important for EU-targeted apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent and automation support.&lt;/strong&gt; APIs built for AI pipelines offer an MCP server or a one-line integration path. Most existing tools were designed before agent frameworks existed and require custom glue code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the options compare
&lt;/h2&gt;

&lt;p&gt;Prices verified June 2026 from each provider's public pricing page, normalized to approximately 10,000 grabs per month. Browserless and Thum.io list a lower per-grab rate than Grabbit; the trade-offs are detailed below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Per grab&lt;/th&gt;
&lt;th&gt;Billing&lt;/th&gt;
&lt;th&gt;Included&lt;/th&gt;
&lt;th&gt;Waste&lt;/th&gt;
&lt;th&gt;Full page&lt;/th&gt;
&lt;th&gt;Wait + selector&lt;/th&gt;
&lt;th&gt;Cookie consent&lt;/th&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grabbit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.002&lt;/td&gt;
&lt;td&gt;Annual flat&lt;/td&gt;
&lt;td&gt;25,000/yr&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;One-line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScreenshotOne&lt;/td&gt;
&lt;td&gt;$0.0079&lt;/td&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;2,000/mo&lt;/td&gt;
&lt;td&gt;Resets&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScrapingBee&lt;/td&gt;
&lt;td&gt;$0.0049&lt;/td&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;~10,000/mo&lt;/td&gt;
&lt;td&gt;Resets&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browserless&lt;/td&gt;
&lt;td&gt;$0.0013&lt;/td&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;20,000 units/mo&lt;/td&gt;
&lt;td&gt;Resets&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Urlbox&lt;/td&gt;
&lt;td&gt;$0.0066&lt;/td&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;2,000/mo&lt;/td&gt;
&lt;td&gt;Resets&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ApiFlash&lt;/td&gt;
&lt;td&gt;$0.0035&lt;/td&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;1,000/mo&lt;/td&gt;
&lt;td&gt;Resets&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScreenshotMachine&lt;/td&gt;
&lt;td&gt;~$0.003&lt;/td&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;2,500/mo&lt;/td&gt;
&lt;td&gt;Resets&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thum.io&lt;/td&gt;
&lt;td&gt;$0.0002&lt;/td&gt;
&lt;td&gt;Usage metered&lt;/td&gt;
&lt;td&gt;$1–$20/mo min&lt;/td&gt;
&lt;td&gt;Minimum&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The options explained
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Grabbit&lt;/strong&gt; is a REST endpoint: POST a URL, get back a hosted &lt;code&gt;image_url&lt;/code&gt;. The $50/yr flat plan includes 25,000 captures that never reset. Full-page, custom viewports (320–1920 px), all three output formats, &lt;code&gt;delay_ms&lt;/code&gt; wait, &lt;code&gt;selector&lt;/code&gt; clipping, and cookie-consent handling are all included. An MCP server lets AI agents call it without any custom integration code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ScreenshotOne&lt;/strong&gt; is the most established provider, with SDK coverage across Ruby, PHP, Go, Java, C#, and more. At $0.0079/grab it is the highest rate in this table, but if your backend is not JavaScript, the maintained first-party SDKs are a genuine advantage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browserless&lt;/strong&gt; has the lowest per-grab rate among full-featured providers at $0.0013. The important caveat: it is a managed headless browser service, not a simple screenshot endpoint. You connect via Puppeteer or Playwright and write your own automation scripts. More powerful than a REST API for complex workflows; more work to set up for a straightforward URL-to-image use case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Urlbox&lt;/strong&gt; has the longest track record and offers enterprise SLAs. At $0.0066/grab it is on the expensive side, but it is a well-maintained product for teams with strict uptime requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ApiFlash and ScrapingBee&lt;/strong&gt; are solid mid-range options. ScrapingBee uses a credit system (approximately 25 credits per grab) and lacks cookie-consent dismissal, which causes captures to be blocked by consent banners in EU-targeted apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thum.io&lt;/strong&gt; has the lowest per-grab rate ($0.0002) with a minimum monthly charge. The trade-off is a limited feature set: no full-page capture, no wait controls, no selector targeting, and no agent support. It works for simple thumbnail generation but not for production automation pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making your first request
&lt;/h2&gt;

&lt;p&gt;Every provider has a different auth scheme and parameter set. Here is a Grabbit request to illustrate the pattern: one endpoint, one auth header, explicit viewport:&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",
    "full_page": false
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes &lt;code&gt;image_url&lt;/code&gt; (the hosted screenshot), &lt;code&gt;bytes&lt;/code&gt;, &lt;code&gt;execution_ms&lt;/code&gt;, and &lt;code&gt;id&lt;/code&gt;. Add &lt;code&gt;delay_ms&lt;/code&gt; (up to 10000) to wait for page content to finish loading, or &lt;code&gt;selector&lt;/code&gt; to clip to a specific element. Switch to a &lt;code&gt;sk_test_&lt;/code&gt; key for development and CI: test captures return a placeholder image at no charge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Billing model matters more than per-grab rate
&lt;/h2&gt;

&lt;p&gt;Monthly subscription comparisons look clean in a table but obscure real cost for variable workloads. A team doing 2,000 grabs in January and 15,000 in March pays the full monthly allowance both months on a reset plan.&lt;/p&gt;

&lt;p&gt;Prepaid credits and annual plans eliminate this: you buy a block and it covers actual usage with nothing evaporating at the end of the cycle. This makes budgeting predictable and avoids the "use it or lose it" pressure that leads teams to pad their capture volume to justify the plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and automation
&lt;/h2&gt;

&lt;p&gt;Most screenshot APIs were designed before agent frameworks existed. The current integration pattern is: get an API key, call the endpoint from backend code. That works, but it requires custom glue code for every agent that needs visual data.&lt;/p&gt;

&lt;p&gt;Agent-native design means the tool registers itself as a callable function in an agent framework. An agent can capture a screenshot and get back a hosted URL to pass downstream with no extra setup. This is the pattern that makes screenshot APIs useful for research agents, monitoring pipelines, and content-generation workflows at scale.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lowest per-grab rate with full features:&lt;/strong&gt; Browserless, but you will write Playwright or Puppeteer scripts rather than calling a REST endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat annual billing with no monthly waste:&lt;/strong&gt; Grabbit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widest SDK coverage for non-JS backends:&lt;/strong&gt; ScreenshotOne.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise SLAs and long track record:&lt;/strong&gt; Urlbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple thumbnails, minimal budget, no features needed:&lt;/strong&gt; Thum.io.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agents or automation pipelines:&lt;/strong&gt; Grabbit, MCP server included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Grabbit's full parameter reference, authentication, and integration guides, see the &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API docs&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/best-screenshot-api" 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>tools</category>
    </item>
    <item>
      <title>Visual Regression Testing: A Practical Guide for 2026</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Wed, 17 Jun 2026 20:52:25 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/visual-regression-testing-a-practical-guide-for-2026-48nk</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/visual-regression-testing-a-practical-guide-for-2026-48nk</guid>
      <description>&lt;p&gt;A functional test confirms the checkout button submits the form. It will not tell you the button is now white on white, overlapping the price, or pushed off the screen on mobile. That is what visual regression testing catches: the layout and styling breakage that passes every assertion and still ships a broken page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is visual regression testing?
&lt;/h2&gt;

&lt;p&gt;Visual regression testing captures screenshots of your interface, compares each new capture against an approved baseline, and flags the pixels that changed. Instead of asserting on the DOM, you assert on what the user actually sees. A change that moves an element, shifts a color, or breaks a responsive layout shows up as a visual diff even when every functional test stays green.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the workflow works
&lt;/h2&gt;

&lt;p&gt;The loop is the same across every tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture a baseline.&lt;/strong&gt; Screenshot the page or component in a known state and store the image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capture on change.&lt;/strong&gt; On each pull request or deploy, capture the same view again.&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;Review.&lt;/strong&gt; If the diff is over your threshold, the test fails and a person looks at it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve or fix.&lt;/strong&gt; If the change was intended, approve it as the new baseline. If not, it is a bug you just caught before users did.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why visual tests get flaky (and how to fix it)
&lt;/h2&gt;

&lt;p&gt;The reason teams abandon visual testing is flakiness: tests that fail on changes nobody made. Almost every false positive traces back to an inconsistent capture, not a real regression. The usual culprits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Animations and transitions&lt;/strong&gt; captured mid-flight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy-loaded images&lt;/strong&gt; or client-rendered content that had not finished when the shot was taken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web fonts&lt;/strong&gt; that loaded a frame late, shifting text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic data&lt;/strong&gt; (timestamps, names, A/B variants) that differs every run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A different viewport&lt;/strong&gt; between baseline and comparison.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is to make capture deterministic: pin the viewport width, wait for the page to settle before capturing, freeze animations, and use stable seed data. Consistency in the capture step is what separates a visual suite people trust from one they mute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Playwright&lt;/strong&gt; has &lt;code&gt;toHaveScreenshot&lt;/code&gt;, which captures and diffs against a stored baseline in a single assertion. The easiest start if you already run Playwright.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storybook&lt;/strong&gt; plus a test runner is a strong fit for component libraries, testing each component in isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosted services&lt;/strong&gt; like Percy and Applitools add a review UI, baseline management, and cross-browser rendering on top of the diff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BackstopJS&lt;/strong&gt; is a long-standing open-source option for page-level scenarios.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick the one that matches how you already build. The capture-and-diff loop is identical underneath. For a fuller rundown of open-source and SaaS options, see &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;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The capture layer: consistent screenshots
&lt;/h2&gt;

&lt;p&gt;Every visual regression tool needs one thing to be reliable: a screenshot that looks the same every time, given the same input. When you are testing deployed URLs (staging, preview deploys, production canaries) rather than local components, a screenshot API gives you that consistent capture without running and scaling headless browsers in CI.&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://staging.example.com/pricing", "width": 1280, "full_page": true, "delay_ms": 500 }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pinning &lt;code&gt;width&lt;/code&gt; keeps the layout identical between runs, &lt;code&gt;full_page&lt;/code&gt; captures the whole document, and &lt;code&gt;delay_ms&lt;/code&gt; waits for content to settle so lazy-loaded sections do not cause false diffs. Store the returned &lt;code&gt;image_url&lt;/code&gt; as your baseline, capture again on each deploy, and diff the two. For running these captures on a schedule or across many URLs, see &lt;a href="https://www.grabbit.live/automated-screenshots" rel="noopener noreferrer"&gt;automated screenshots&lt;/a&gt;, and for every capture option see the &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;If you already use Playwright, add one &lt;code&gt;toHaveScreenshot&lt;/code&gt; assertion to your most important page and watch it for a week. If you test deployed URLs, capture consistent baselines with an API and diff them in CI. Either way, start with a handful of high-value screens, get the captures deterministic, and expand once the suite is quiet enough to trust.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/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 Take a Full-Page Screenshot (Every Method That Works in 2026)</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Wed, 17 Jun 2026 17:49:00 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/how-to-take-a-full-page-screenshot-every-method-that-works-in-2026-33la</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/how-to-take-a-full-page-screenshot-every-method-that-works-in-2026-33la</guid>
      <description>&lt;p&gt;A regular screenshot stops at the fold: it captures only what is currently on your screen. A full-page screenshot captures the entire document, from the top of the page to the bottom, no matter how far it scrolls. Here is how to do it on every platform, plus how to capture full pages automatically when you need more than one.&lt;/p&gt;

&lt;p&gt;The header image above was captured from grabbit.live with the Grabbit API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quickest answer per platform
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Fastest method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome&lt;/td&gt;
&lt;td&gt;DevTools Command Menu, "Capture full size screenshot"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefox&lt;/td&gt;
&lt;td&gt;Right-click, "Take Screenshot", "Save full page"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge&lt;/td&gt;
&lt;td&gt;Web capture (Ctrl+Shift+S), "Capture full page"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;Use the browser methods above (Snipping Tool is viewport only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac&lt;/td&gt;
&lt;td&gt;Use the browser methods above (no native scroll capture)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPhone&lt;/td&gt;
&lt;td&gt;Screenshot, then the "Full Page" tab, save as PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;Screenshot, then "Capture more"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Chrome (built in, no extension)
&lt;/h2&gt;

&lt;p&gt;Chrome can capture a full page without any add-on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the page and press F12 to open DevTools.&lt;/li&gt;
&lt;li&gt;Open the Command Menu with Ctrl+Shift+P (Cmd+Shift+P on Mac).&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;screenshot&lt;/code&gt; and choose &lt;strong&gt;Capture full size screenshot&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Chrome scrolls the page for you and saves one tall image to your downloads. This is the most reliable manual method on any operating system, since it works the same on Windows, Mac, and Linux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chrome and Edge with an extension
&lt;/h2&gt;

&lt;p&gt;If you take full-page shots often, a one-click extension is faster. GoFullPage and similar extensions add a toolbar button that scrolls, stitches, and exports the page as PNG or PDF. Useful for occasional manual captures, though every extension you install gets access to the pages you visit, so weigh that against convenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Firefox (built in)
&lt;/h2&gt;

&lt;p&gt;Firefox has a native full-page tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Right-click anywhere on the page.&lt;/li&gt;
&lt;li&gt;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;/ol&gt;

&lt;h2&gt;
  
  
  Edge (built in)
&lt;/h2&gt;

&lt;p&gt;Microsoft Edge ships Web Capture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press Ctrl+Shift+S, or open the menu and choose &lt;strong&gt;Web capture&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Capture full page&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Annotate if you want, then save.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Windows and the Snipping Tool
&lt;/h2&gt;

&lt;p&gt;A common question: can the Snipping Tool take a scrolling screenshot? No. The Windows Snipping Tool and PrtScn only capture the visible area. For a full page on Windows, use Chrome's DevTools capture, Edge's Web Capture, or an extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mac
&lt;/h2&gt;

&lt;p&gt;macOS shortcuts (Cmd+Shift+3 for the whole screen, Cmd+Shift+4 for a selection, Cmd+Shift+5 for the toolbar) all capture only what is visible. There is no native scrolling-screenshot tool, so for a full page use your browser's built-in capture as described above.&lt;/p&gt;

&lt;h2&gt;
  
  
  iPhone and Android
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iPhone:&lt;/strong&gt; take a screenshot (side button plus volume up), tap the preview thumbnail, then switch to the &lt;strong&gt;Full Page&lt;/strong&gt; tab at the top. Save the entire page as a PDF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android:&lt;/strong&gt; take a screenshot, then tap &lt;strong&gt;Capture more&lt;/strong&gt; (the exact label varies by manufacturer) to extend the capture down the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Capturing full pages automatically with an API
&lt;/h2&gt;

&lt;p&gt;Manual methods are fine for one page. When you need to capture full pages on a schedule, across many URLs, or inside an app, an API does it with a single request. Set &lt;code&gt;full_page&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; and the capture spans the entire document height:&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", "full_page": true, "format": "webp" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response returns a hosted &lt;code&gt;image_url&lt;/code&gt; for the full page. Add &lt;code&gt;delay_ms&lt;/code&gt; (up to 10000) to wait for lazy-loaded content, &lt;code&gt;width&lt;/code&gt; (320 to 1920) to set the layout, or a CSS &lt;code&gt;selector&lt;/code&gt; to clip to one element. For the full walkthrough, see the &lt;a href="https://www.grabbit.live/guides/full-page-screenshot" rel="noopener noreferrer"&gt;full-page screenshot guide&lt;/a&gt;, and for every parameter see the &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;screenshot API&lt;/a&gt;. If you only need a normal capture, our &lt;a href="https://www.grabbit.live/guides/screenshot-a-website" rel="noopener noreferrer"&gt;guide to screenshotting any website&lt;/a&gt; covers the basics.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/full-page-screenshot" 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>Open Graph Image Sizes and Dimensions: The Complete 2026 Guide</title>
      <dc:creator>Nico Acosta</dc:creator>
      <pubDate>Tue, 16 Jun 2026 22:19:38 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/grabbit/open-graph-image-sizes-and-dimensions-the-complete-2026-guide-1k16</link>
      <guid>https://dev.clauneck.workers.dev/grabbit/open-graph-image-sizes-and-dimensions-the-complete-2026-guide-1k16</guid>
      <description>&lt;p&gt;When you paste a link into Slack, X, or iMessage and it unfurls into a rich card with a big image, that image comes from your page's Open Graph tags. Get the size wrong and the card collapses into a tiny thumbnail or, worse, shows nothing at all. This guide gives you the exact dimensions and the meta tags to use.&lt;/p&gt;

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

&lt;p&gt;Use &lt;strong&gt;1200 by 630 pixels&lt;/strong&gt;. That is an aspect ratio of roughly &lt;strong&gt;1.91 to 1&lt;/strong&gt;, and it is the size every major platform renders as a full-width preview card. If you only remember one number, remember 1200 by 630.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform-by-platform sizes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Recommended size&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Facebook&lt;/td&gt;
&lt;td&gt;1200 x 630&lt;/td&gt;
&lt;td&gt;Under 600 x 315 drops to a small thumbnail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X (Twitter)&lt;/td&gt;
&lt;td&gt;1200 x 630&lt;/td&gt;
&lt;td&gt;Needs &lt;code&gt;twitter:card&lt;/code&gt; set to &lt;code&gt;summary_large_image&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn&lt;/td&gt;
&lt;td&gt;1200 x 627&lt;/td&gt;
&lt;td&gt;Treats 1200 x 630 the same in practice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack&lt;/td&gt;
&lt;td&gt;1200 x 630&lt;/td&gt;
&lt;td&gt;Reads standard &lt;code&gt;og:image&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discord&lt;/td&gt;
&lt;td&gt;1200 x 630&lt;/td&gt;
&lt;td&gt;Reads standard &lt;code&gt;og:image&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iMessage&lt;/td&gt;
&lt;td&gt;1200 x 630&lt;/td&gt;
&lt;td&gt;Reads standard &lt;code&gt;og:image&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The takeaway: one 1200 by 630 image satisfies all of them. You do not need a different file per platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimum, maximum, and file size
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimum for a large card:&lt;/strong&gt; 600 by 315. Below this, Facebook and LinkedIn render a small square thumbnail next to the text instead of the big card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aspect ratio:&lt;/strong&gt; keep it at 1.91 to 1. If your image is a different ratio, platforms crop it, usually from the center, and your text can get cut off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File size:&lt;/strong&gt; keep it under 8 MB for Facebook, but aim for well under 1 MB so the card loads fast. WebP or optimized PNG and JPEG all work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe zone:&lt;/strong&gt; keep important text and logos away from the outer edges, since some apps crop a few pixels.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The meta tags you actually need
&lt;/h2&gt;

&lt;p&gt;Put these in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of every page you want to share:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:title"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Your page title"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"A one-line summary."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://yoursite.com/og/your-page.png"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image:width"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image:height"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"twitter:card"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"summary_large_image"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"twitter:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://yoursite.com/og/your-page.png"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;og:image:width&lt;/code&gt; and &lt;code&gt;og:image:height&lt;/code&gt; is the detail most people skip. It lets the platform reserve the right space and render the large card immediately instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to generate OG images from a URL
&lt;/h2&gt;

&lt;p&gt;Designing a unique image for every page by hand does not scale. The pattern that does: build one small HTML template that renders the title and your branding, then &lt;strong&gt;capture that page as an image&lt;/strong&gt; at 1200 by 630.&lt;/p&gt;

&lt;p&gt;A screenshot API turns that into a single request. Point it at your template URL with a fixed viewport:&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/og/my-post", "width": 1200, "height": 630 }'&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; you can drop straight into your &lt;code&gt;og:image&lt;/code&gt; tag. Because the source is a real web page, you can style it with the same CSS as the rest of your site, pull in dynamic titles, and never open a design tool. This is exactly what &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;Grabbit's screenshot API&lt;/a&gt; is built for, and you can wire it up with a free test key first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using a logo as the OG image.&lt;/strong&gt; Logos are square and get cropped or shrunk. Use a 1200 by 630 card with the title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting &lt;code&gt;summary_large_image&lt;/code&gt; for X.&lt;/strong&gt; Without it, X shows the small card no matter how big your image is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A relative image URL.&lt;/strong&gt; &lt;code&gt;og:image&lt;/code&gt; must be an absolute URL, including &lt;code&gt;https://&lt;/code&gt;. Crawlers do not resolve relative paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not testing.&lt;/strong&gt; Always preview the card before you publish. See the FAQ for how.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;One size, 1200 by 630, covers every major platform. Set the width and height tags, use an absolute URL, add &lt;code&gt;summary_large_image&lt;/code&gt; for X, and generate the image from an HTML template so it scales across your whole site. For automating that last step, see &lt;a href="https://www.grabbit.live/screenshot-api" rel="noopener noreferrer"&gt;how to generate dynamic OG images from any URL&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.grabbit.live/blog/og-image-sizes" 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>
  </channel>
</rss>
