<?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: Corbanware</title>
    <description>The latest articles on DEV Community by Corbanware (@corbanware).</description>
    <link>https://dev.to/corbanware</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3773444%2Faf1e64c2-072e-4647-b4df-a2b0da0dc2fc.jpg</url>
      <title>DEV Community: Corbanware</title>
      <link>https://dev.to/corbanware</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/corbanware"/>
    <language>en</language>
    <item>
      <title>OG Image Sizes: The Complete Guide for 2026</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 11 May 2026 01:00:58 +0000</pubDate>
      <link>https://dev.to/corbanware/og-image-sizes-the-complete-guide-for-2026-4l6d</link>
      <guid>https://dev.to/corbanware/og-image-sizes-the-complete-guide-for-2026-4l6d</guid>
      <description>&lt;h1&gt;
  
  
  OG Image Sizes: The Complete Guide for 2026
&lt;/h1&gt;

&lt;p&gt;Here is the quick answer: &lt;strong&gt;1200 x 630 pixels&lt;/strong&gt;. If you remember nothing else from this article, remember that number. It works on every major platform without cropping or distortion.&lt;/p&gt;

&lt;p&gt;But if you want to understand why, what the edge cases are, and how to avoid the mistakes that make your shared links look broken, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference Table
&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;Min Size&lt;/th&gt;
&lt;th&gt;Aspect Ratio&lt;/th&gt;
&lt;th&gt;Max File Size&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;600 x 315&lt;/td&gt;
&lt;td&gt;1.91:1&lt;/td&gt;
&lt;td&gt;8 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twitter/X (large card)&lt;/td&gt;
&lt;td&gt;1200 x 628&lt;/td&gt;
&lt;td&gt;300 x 157&lt;/td&gt;
&lt;td&gt;2:1&lt;/td&gt;
&lt;td&gt;5 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twitter/X (summary)&lt;/td&gt;
&lt;td&gt;240 x 240&lt;/td&gt;
&lt;td&gt;144 x 144&lt;/td&gt;
&lt;td&gt;1:1&lt;/td&gt;
&lt;td&gt;5 MB&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;200 x 200&lt;/td&gt;
&lt;td&gt;1.91:1&lt;/td&gt;
&lt;td&gt;5 MB&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;Any&lt;/td&gt;
&lt;td&gt;1.91:1&lt;/td&gt;
&lt;td&gt;No hard limit&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;Any&lt;/td&gt;
&lt;td&gt;1.91:1&lt;/td&gt;
&lt;td&gt;8 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp&lt;/td&gt;
&lt;td&gt;1200 x 630&lt;/td&gt;
&lt;td&gt;300 x 200&lt;/td&gt;
&lt;td&gt;1.91:1&lt;/td&gt;
&lt;td&gt;5 MB&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;Any&lt;/td&gt;
&lt;td&gt;1.91:1&lt;/td&gt;
&lt;td&gt;No hard limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pinterest&lt;/td&gt;
&lt;td&gt;1200 x 630&lt;/td&gt;
&lt;td&gt;200 x 200&lt;/td&gt;
&lt;td&gt;1.91:1&lt;/td&gt;
&lt;td&gt;10 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bookmark this table. Reference it when you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 1200 x 630 Is the Standard
&lt;/h2&gt;

&lt;p&gt;Back in 2010, Facebook introduced the Open Graph protocol to let websites control how shared links appear in the News Feed. They settled on a roughly 1.91:1 aspect ratio for link preview images. Every other platform that came along afterward -- Twitter, LinkedIn, Slack, Discord -- adopted the same ratio or something close enough that a 1200x630 image displays without issues.&lt;/p&gt;

&lt;p&gt;The result: one image at 1200 x 630 works everywhere. You do not need platform-specific images. Period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 1200px wide specifically?&lt;/strong&gt; Because it renders crisply on high-DPI (Retina) displays. On a standard feed where link cards are displayed at around 600px wide, a 1200px source image means 2x pixel density. Your text stays sharp, your gradients stay smooth.&lt;/p&gt;

&lt;h2&gt;
  
  
  PNG vs. JPEG: Which Format to Use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PNG&lt;/strong&gt; -- Use for images with text, solid colors, or sharp edges. Most OG images fall into this category.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JPEG&lt;/strong&gt; -- Use for photographic images. JPEG compresses these more efficiently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebP&lt;/strong&gt; -- Inconsistent support across platforms. Stick to PNG or JPEG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG&lt;/strong&gt; -- Do not use SVG for OG images. Most platforms will silently ignore it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Safe Zone: Where to Place Your Content
&lt;/h2&gt;

&lt;p&gt;Platforms do not all crop identically. Keep all important content within a &lt;strong&gt;60px margin&lt;/strong&gt; safe zone on all sides. That means your actual content area is roughly 1080 x 510 pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Font Size Matters More Than You Think
&lt;/h2&gt;

&lt;p&gt;OG images are typically displayed at 400-600 pixels wide in feeds. Rules of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title text:&lt;/strong&gt; Minimum 48px at 1200px width&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description text:&lt;/strong&gt; Minimum 28px at 1200px width&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit text to 2-3 lines&lt;/strong&gt; for the title&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High contrast:&lt;/strong&gt; White text on dark backgrounds or vice versa&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes (and How to Fix Them)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Using a square image&lt;/strong&gt; -- A 1:1 image will get aggressively cropped. Always use 1200 x 630.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. File size too large&lt;/strong&gt; -- Aim for under 1 MB. Use TinyPNG or ImageOptim to compress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Relative URLs in the meta tag&lt;/strong&gt; -- Social platform crawlers need the full absolute URL starting with https://.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Caching issues&lt;/strong&gt; -- Platforms cache OG images aggressively. Use each platform's debugger tool to bust the cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Missing width and height meta tags&lt;/strong&gt; -- Providing og:image:width and og:image:height helps platforms render previews faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Test Your OG Images
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Facebook Sharing Debugger&lt;/strong&gt; -- Paste your URL, click Scrape Again&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter Card Validator&lt;/strong&gt; -- Preview the card rendering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn Post Inspector&lt;/strong&gt; -- Check LinkedIn's cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;opengraph.xyz&lt;/strong&gt; -- Previews across multiple platforms simultaneously&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Automating OG Image Generation
&lt;/h2&gt;

&lt;p&gt;For sites with many pages, manual OG image creation does not scale. Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;@vercel/og&lt;/strong&gt; -- JSX to PNG on the edge. Free, self-hosted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudinary&lt;/strong&gt; -- Text overlay capabilities for dynamic transformations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ogimg.xyz&lt;/strong&gt; -- Dedicated API for OG image generation. Free tier with 50 images/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puppeteer/Playwright&lt;/strong&gt; -- Screenshot approach. Flexible but slow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The One-Sentence Summary
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;1200 x 630 pixels&lt;/strong&gt;, &lt;strong&gt;PNG format&lt;/strong&gt;, &lt;strong&gt;under 1 MB&lt;/strong&gt;, with text inside a &lt;strong&gt;60px safe zone&lt;/strong&gt;, and always test with platform debugger tools before you ship.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? Follow me for more practical web development guides.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>socialmedia</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to Add OG Images to Next.js in 5 Minutes</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 27 Apr 2026 01:01:38 +0000</pubDate>
      <link>https://dev.to/corbanware/how-to-add-og-images-to-nextjs-in-5-minutes-45nj</link>
      <guid>https://dev.to/corbanware/how-to-add-og-images-to-nextjs-in-5-minutes-45nj</guid>
      <description>&lt;h1&gt;
  
  
  How to Add OG Images to Next.js in 5 Minutes
&lt;/h1&gt;

&lt;p&gt;If you have ever shared a link on Twitter or Slack and watched it render as a sad, plain-text URL with no preview image, you already understand why OG images matter. Those 1200x630 pixel cards are the difference between a link that gets clicked and one that gets scrolled past. Studies show that links with rich preview images get 2-3x more engagement than bare text links.&lt;/p&gt;

&lt;p&gt;The good news: if you are building with Next.js and the App Router, adding OG images is remarkably straightforward. Let me walk you through three approaches, from the simplest to the most powerful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Foundation: Understanding OG Meta Tags
&lt;/h2&gt;

&lt;p&gt;Before we touch any Next.js code, here is what the browser needs to see in your HTML &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&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.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: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 brief description"&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;That is it. Every social platform (Facebook, Twitter/X, LinkedIn, Slack, Discord, WhatsApp) reads these tags to build the preview card. The image URL &lt;strong&gt;must&lt;/strong&gt; be absolute -- relative paths will not work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: Static Metadata (30 Seconds)
&lt;/h2&gt;

&lt;p&gt;For pages where the OG image never changes (your homepage, about page, pricing page), use the &lt;code&gt;metadata&lt;/code&gt; export in your layout or page file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Metadata&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="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Awesome App&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The best thing since sliced bread&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Awesome App&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The best thing since sliced bread&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://yoursite.com/og-homepage.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;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;alt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Awesome App - homepage preview&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;twitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;summary_large_image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next.js automatically renders the correct &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags in the HTML head. No manual string concatenation, no &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;. It even handles the Twitter card fallback for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Always include the &lt;code&gt;twitter.card: "summary_large_image"&lt;/code&gt; property. Without it, Twitter defaults to the small square card, which wastes most of your carefully designed image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: Dynamic generateMetadata (2 Minutes)
&lt;/h2&gt;

&lt;p&gt;This is where things get interesting. For blogs, documentation, or any page with dynamic content, you want a unique OG image per page. The &lt;code&gt;generateMetadata&lt;/code&gt; async function lets you compute metadata at request time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Metadata&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="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPostBySlug&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="s2"&gt;@/lib/posts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Metadata&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&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;post&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;getPostBySlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&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="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&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="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&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="s2"&gt;article&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;publishedTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publishedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="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="s2"&gt;`https://yoursite.com/api/og?title=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;twitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;summary_large_image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the OG image URL points to an API route. That brings us to the next question: how do you actually &lt;strong&gt;generate&lt;/strong&gt; the image?&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 3: Generating Images on the Fly
&lt;/h2&gt;

&lt;p&gt;You have two main options here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: @vercel/og (Self-Hosted)
&lt;/h3&gt;

&lt;p&gt;Vercel's @vercel/og library lets you write JSX that renders as an image. It uses Satori under the hood -- a layout engine that converts React components to SVG, then rasterizes to PNG.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/og/route.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ImageResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/og&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt; &lt;span class="p"&gt;}&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Blog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linear-gradient(135deg, #667eea 0%, #764ba2 100%)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;textAlign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well if you want full control over the design and are comfortable with Satori's CSS limitations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: External API
&lt;/h3&gt;

&lt;p&gt;If you would rather not maintain your own image generation endpoint, an external API simplifies things significantly. For example, with &lt;a href="https://ogimg.xyz?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=wf5" rel="noopener noreferrer"&gt;ogimg.xyz&lt;/a&gt;, your dynamic metadata becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ogUrl&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="s2"&gt;https://ogimg.xyz/api/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;utm_source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;devto&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;utm_medium&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;utm_campaign&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;wf5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ogUrl&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="s2"&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;post&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;ogUrl&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="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ogUrl&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="s2"&gt;template&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="s2"&gt;gradient&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;No API route to maintain, no font files to bundle, no Satori quirks to debug. ogimg.xyz offers a free tier of 50 images/month, which is enough for most personal blogs and small projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your OG Images
&lt;/h2&gt;

&lt;p&gt;After deploying, always verify your images actually work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Facebook Sharing Debugger&lt;/strong&gt; -- Paste your URL and hit Scrape Again to force a fresh fetch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter Card Validator&lt;/strong&gt; -- Preview exactly what Twitter will render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn Post Inspector&lt;/strong&gt; -- Check how LinkedIn will render your shared link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open your browser DevTools&lt;/strong&gt; -- Search the HTML head for og:image and make sure the URL loads.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Quick Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;og:image URL is absolute (starts with https://)&lt;/li&gt;
&lt;li&gt;Image dimensions are 1200x630&lt;/li&gt;
&lt;li&gt;File size is under 1MB (5MB max for most platforms)&lt;/li&gt;
&lt;li&gt;twitter:card is set to summary_large_image&lt;/li&gt;
&lt;li&gt;Image renders correctly when you open the URL directly&lt;/li&gt;
&lt;li&gt;You have tested with at least one social platform debugger&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Adding proper OG images to a Next.js app is one of those 5-minute tasks that pays dividends every time someone shares your content. Whether you go with static metadata, dynamic generateMetadata, self-hosted @vercel/og, or an external service, the important thing is that you do it.&lt;/p&gt;

&lt;p&gt;Your links deserve better than plain text.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions about OG images in Next.js? Drop a comment below -- I read every one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>GEO vs SEO: Why Optimizing for AI Search Is the Next Frontier</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 20 Apr 2026 01:00:50 +0000</pubDate>
      <link>https://dev.to/corbanware/geo-vs-seo-why-optimizing-for-ai-search-is-the-next-frontier-13f</link>
      <guid>https://dev.to/corbanware/geo-vs-seo-why-optimizing-for-ai-search-is-the-next-frontier-13f</guid>
      <description></description>
      <category>webdev</category>
      <category>ai</category>
      <category>saas</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why Your Brand Is Invisible to ChatGPT (And How to Fix It)</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 13 Apr 2026 01:00:49 +0000</pubDate>
      <link>https://dev.to/corbanware/why-your-brand-is-invisible-to-chatgpt-and-how-to-fix-it-2721</link>
      <guid>https://dev.to/corbanware/why-your-brand-is-invisible-to-chatgpt-and-how-to-fix-it-2721</guid>
      <description></description>
      <category>webdev</category>
      <category>ai</category>
      <category>saas</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built an AI Search Visibility Auditor - Here Is What I Learned</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 06 Apr 2026 01:00:26 +0000</pubDate>
      <link>https://dev.to/corbanware/i-built-an-ai-search-visibility-auditor-here-is-what-i-learned-3j85</link>
      <guid>https://dev.to/corbanware/i-built-an-ai-search-visibility-auditor-here-is-what-i-learned-3j85</guid>
      <description></description>
      <category>webdev</category>
      <category>ai</category>
      <category>saas</category>
      <category>programming</category>
    </item>
    <item>
      <title>GEO vs SEO: Why Optimizing for AI Search Is the Next Frontier</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 30 Mar 2026 01:00:36 +0000</pubDate>
      <link>https://dev.to/corbanware/geo-vs-seo-why-optimizing-for-ai-search-is-the-next-frontier-254d</link>
      <guid>https://dev.to/corbanware/geo-vs-seo-why-optimizing-for-ai-search-is-the-next-frontier-254d</guid>
      <description></description>
      <category>webdev</category>
      <category>ai</category>
      <category>saas</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why Your Brand Is Invisible to ChatGPT (And How to Fix It)</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 23 Mar 2026 01:00:31 +0000</pubDate>
      <link>https://dev.to/corbanware/why-your-brand-is-invisible-to-chatgpt-and-how-to-fix-it-3d25</link>
      <guid>https://dev.to/corbanware/why-your-brand-is-invisible-to-chatgpt-and-how-to-fix-it-3d25</guid>
      <description></description>
      <category>webdev</category>
      <category>ai</category>
      <category>saas</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built an AI Search Visibility Auditor - Here Is What I Learned</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 16 Mar 2026 01:01:11 +0000</pubDate>
      <link>https://dev.to/corbanware/i-built-an-ai-search-visibility-auditor-here-is-what-i-learned-1c58</link>
      <guid>https://dev.to/corbanware/i-built-an-ai-search-visibility-auditor-here-is-what-i-learned-1c58</guid>
      <description></description>
      <category>webdev</category>
      <category>ai</category>
      <category>saas</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a Multi-LLM Fallback System with Vercel AI SDK</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 09 Mar 2026 01:01:20 +0000</pubDate>
      <link>https://dev.to/corbanware/building-a-multi-llm-fallback-system-with-vercel-ai-sdk-1ijo</link>
      <guid>https://dev.to/corbanware/building-a-multi-llm-fallback-system-with-vercel-ai-sdk-1ijo</guid>
      <description>&lt;p&gt;A practical guide to implementing LLM provider fallback chains for production applications. Covers the architecture of Geonapse's 3-tier fallback system (Gemini Flash-Lite ? Groq ? DeepSeek), error handling patterns, cost optimization strategies, response quality validation, and monitoring. Includes code examples using Vercel AI SDK's unified interface and lessons learned from running multi-provider LLM systems in production.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>saas</category>
      <category>programming</category>
    </item>
    <item>
      <title>GEO vs SEO: Why Your Website Is Invisible to ChatGPT (And How to Fix It)</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 02 Mar 2026 01:01:17 +0000</pubDate>
      <link>https://dev.to/corbanware/geo-vs-seo-why-your-website-is-invisible-to-chatgpt-and-how-to-fix-it-57pi</link>
      <guid>https://dev.to/corbanware/geo-vs-seo-why-your-website-is-invisible-to-chatgpt-and-how-to-fix-it-57pi</guid>
      <description>&lt;p&gt;Generative Engine Optimization (GEO) is emerging as the next evolution of SEO. This article explores why traditional SEO techniques only partially work for AI search engines, what factors influence LLM brand recommendations, and a practical framework for auditing your AI search visibility. Covers knowledge graph presence, structured data importance, citation cascades, content structure optimization, and the growing importance of platform presence across Wikipedia, Crunchbase, G2, and other knowledge sources.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>saas</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built an AI Search Visibility Auditor Here's the Architecture</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Mon, 23 Feb 2026 01:01:07 +0000</pubDate>
      <link>https://dev.to/corbanware/i-built-an-ai-search-visibility-auditor-heres-the-architecture-7fb</link>
      <guid>https://dev.to/corbanware/i-built-an-ai-search-visibility-auditor-heres-the-architecture-7fb</guid>
      <description>&lt;p&gt;A technical deep-dive into building Geonapse, an AI search visibility auditor. Covers the 3-tier LLM fallback architecture (Gemini Flash-Lite ? Groq ? DeepSeek), the 40+ check audit engine, ranking detection via AI search APIs, and platform presence scanning across 8 knowledge platforms. Built with Next.js 16, Vercel AI SDK, Neon PostgreSQL, and Drizzle ORM. Includes lessons learned about LLM reliability, cost optimization, and building evaluation frameworks for AI-generated content.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>saas</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built an OG Image Generator API — Here's How (Next.js + Vercel Edge)</title>
      <dc:creator>Corbanware</dc:creator>
      <pubDate>Sun, 15 Feb 2026 13:56:47 +0000</pubDate>
      <link>https://dev.to/corbanware/i-built-an-og-image-generator-api-heres-how-nextjs-vercel-edge-1dg2</link>
      <guid>https://dev.to/corbanware/i-built-an-og-image-generator-api-heres-how-nextjs-vercel-edge-1dg2</guid>
      <description>&lt;p&gt;Every web page that gets shared on social media needs an Open Graph image. It's that 1200x630 preview card you see on Twitter, Facebook, LinkedIn, and Slack. Without one, your shared links look like plain text. With a good one, click-through rates go up significantly.&lt;/p&gt;

&lt;p&gt;I've been building web projects for years, and creating OG images has always been my least favorite part of the process. Open Figma, create a canvas, add text, pick colors, export, upload. Repeat for every page. For a blog with 30 posts, that's 30 images. For a documentation site with 100 pages, it's simply not viable.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://ogimg.xyz?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=technical_article" rel="noopener noreferrer"&gt;ogimg.xyz&lt;/a&gt; — an API that generates OG images programmatically. Send a POST request, get back a PNG. Here's how it works under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture
&lt;/h3&gt;

&lt;p&gt;The entire application is a Next.js 15 project deployed on Vercel. The image generation endpoint is a Vercel Edge Function, which means it runs on Cloudflare's network close to the user — no cold starts, no long-running server processes.&lt;/p&gt;

&lt;p&gt;The core pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request → Validate API Key → Resolve Template → Satori (JSX → SVG) → Resvg (SVG → PNG) → Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me break down each step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Request Validation
&lt;/h3&gt;

&lt;p&gt;Every API call requires an API key passed via the &lt;code&gt;Authorization: Bearer&lt;/code&gt; header. The key is looked up in a Neon PostgreSQL database (via Drizzle ORM). I check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the key valid and active?&lt;/li&gt;
&lt;li&gt;What plan tier is this user on?&lt;/li&gt;
&lt;li&gt;Have they exceeded their monthly quota?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rate limiting uses a sliding window counter. Free users get 50 images/month, Hobby gets 1,000, and Pro gets 10,000.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://ogimg.xyz/api/generate?utm_source&lt;span class="o"&gt;=&lt;/span&gt;devto&amp;amp;utm_medium&lt;span class="o"&gt;=&lt;/span&gt;article&amp;amp;utm_campaign&lt;span class="o"&gt;=&lt;/span&gt;wf5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer og_live_abc123..."&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;'{
    "title": "My Awesome Blog Post",
    "description": "Learn how to build better software",
    "template": "gradient",
    "backgroundColor": "#0f172a",
    "textColor": "#f8fafc"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: The Template System
&lt;/h3&gt;

&lt;p&gt;Each template is a React component. Yes, actual JSX — because the rendering engine (Satori) understands React components.&lt;/p&gt;

&lt;p&gt;Here's a simplified version of what a template looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GradientTemplate&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;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;TemplateProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`linear-gradient(135deg, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;backgroundColor&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="nf"&gt;adjustColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&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="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PatternOverlay&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;marginTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="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;There are 10 templates total: gradient, minimal, bold, split, centered, documentation, dark, light, branded, and announcement. Each is designed for a different use case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Background Patterns
&lt;/h3&gt;

&lt;p&gt;I built 10 SVG-based background patterns: dots, grid, diagonal lines, cross-hatch, waves, circles, hexagons, triangles, diamonds, and noise. They're rendered as inline SVG within the template and can be customized with color and opacity.&lt;/p&gt;

&lt;p&gt;The pattern is layered behind the text content using Flexbox positioning (Satori doesn't support &lt;code&gt;position: absolute&lt;/code&gt;, so everything is Flexbox-based).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Satori — JSX to SVG
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/vercel/satori" rel="noopener noreferrer"&gt;Satori&lt;/a&gt; is Vercel's library for converting React components (JSX) to SVG. It implements a subset of CSS Flexbox and renders text using actual font files (loaded as ArrayBuffers).&lt;/p&gt;

&lt;p&gt;Key limitations I had to work around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;position: absolute&lt;/code&gt;&lt;/strong&gt;: All layout must use Flexbox. This makes some designs trickier but keeps the rendering engine simple and fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited CSS properties&lt;/strong&gt;: No &lt;code&gt;box-shadow&lt;/code&gt;, no &lt;code&gt;backdrop-filter&lt;/code&gt;, no CSS gradients on text. You work within the constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font loading&lt;/strong&gt;: You must provide font files explicitly. I bundle Inter and a few other Google Fonts as static assets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Despite the limitations, Satori is fast. It converts a template to SVG in about 50-100ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: SVG to PNG
&lt;/h3&gt;

&lt;p&gt;The SVG from Satori is converted to PNG using Resvg (via &lt;code&gt;@vercel/og&lt;/code&gt;). This is a Rust-based SVG renderer compiled to WebAssembly, so it runs on the Edge without any native dependencies. The PNG conversion takes another 50-150ms.&lt;/p&gt;

&lt;p&gt;The total pipeline from request to response is typically 200-500ms, depending on complexity and edge location.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: The Watermark
&lt;/h3&gt;

&lt;p&gt;Free tier images get a small "Powered by ogimg.xyz" watermark in the bottom-right corner. It's rendered as part of the template — just an additional text element added conditionally based on the user's plan tier. Paid plans remove it entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  URL Auto-Fetch Mode
&lt;/h3&gt;

&lt;p&gt;There's a convenience feature where you send a URL instead of content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://ogimg.xyz/api/generate?utm_source&lt;span class="o"&gt;=&lt;/span&gt;devto&amp;amp;utm_medium&lt;span class="o"&gt;=&lt;/span&gt;article&amp;amp;utm_campaign&lt;span class="o"&gt;=&lt;/span&gt;wf5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer og_live_abc123..."&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/blog/my-post",
    "template": "minimal"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API fetches the URL, parses the HTML for &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;meta name="description"&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;link rel="icon"&amp;gt;&lt;/code&gt;, and uses those as the template input.&lt;/p&gt;

&lt;p&gt;Important security note: I implemented SSRF protection here. The fetch is restricted to public IP ranges only (no &lt;code&gt;127.0.0.1&lt;/code&gt;, no &lt;code&gt;10.x.x.x&lt;/code&gt;, no &lt;code&gt;169.254.x.x&lt;/code&gt;). Schemes are limited to &lt;code&gt;http&lt;/code&gt; and &lt;code&gt;https&lt;/code&gt;. There's a 5-second timeout. If you're building something similar that accepts user-provided URLs, don't skip this step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Billing Integration
&lt;/h3&gt;

&lt;p&gt;I use &lt;a href="https://www.lemonsqueezy.com/" rel="noopener noreferrer"&gt;LemonSqueezy&lt;/a&gt; as the merchant of record. This means LemonSqueezy handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment processing (Stripe under the hood)&lt;/li&gt;
&lt;li&gt;Global tax calculation and remittance (VAT, GST, sales tax)&lt;/li&gt;
&lt;li&gt;Invoicing&lt;/li&gt;
&lt;li&gt;Subscription management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On my end, I listen for LemonSqueezy webhooks to update user plan tiers in my database. When a user upgrades, their quota and feature access update immediately.&lt;/p&gt;

&lt;p&gt;Pricing tiers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Images/Month&lt;/th&gt;
&lt;th&gt;Templates&lt;/th&gt;
&lt;th&gt;Watermark&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hobby&lt;/td&gt;
&lt;td&gt;$4.90/mo&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$9.90/mo&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifetime&lt;/td&gt;
&lt;td&gt;$149 once&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Live Playground
&lt;/h3&gt;

&lt;p&gt;I built an interactive playground at &lt;a href="https://ogimg.xyz?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=technical_article" rel="noopener noreferrer"&gt;ogimg.xyz&lt;/a&gt; where you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select a template from a visual grid&lt;/li&gt;
&lt;li&gt;Enter your title and description&lt;/li&gt;
&lt;li&gt;Pick background colors and patterns&lt;/li&gt;
&lt;li&gt;See the preview update in real-time&lt;/li&gt;
&lt;li&gt;Copy the equivalent API call (curl or JavaScript)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This serves two purposes: it lets non-technical users generate images without writing code, and it lets developers experiment before integrating the API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons Learned
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Satori is great but limited.&lt;/strong&gt; If you need pixel-perfect designs, you'll hit its CSS limitations. For structured templates with text and simple graphics, it's perfect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edge rendering is the right call for this use case.&lt;/strong&gt; No cold starts, globally distributed, and fast enough for real-time generation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Freemium works for developer tools.&lt;/strong&gt; Developers need to test before they commit. A generous free tier with a clear upgrade path converts better than a free trial with a time limit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SSRF protection is essential.&lt;/strong&gt; If your API accepts user-provided URLs, protect against server-side request forgery from day one. Not later. Now.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Try It
&lt;/h3&gt;

&lt;p&gt;Head over to &lt;a href="https://ogimg.xyz?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=technical_article" rel="noopener noreferrer"&gt;ogimg.xyz&lt;/a&gt; and generate your first image. The free tier gives you 50 images/month with no credit card required. If you're integrating into a Next.js project, it's literally one fetch call:&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://ogimg.xyz/api/generate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,?&lt;/span&gt;&lt;span class="nx"&gt;utm_source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;devto&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;utm_medium&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;utm_campaign&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;wf5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&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="nx"&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;OGIMG_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="nx"&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Page Title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A brief description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gradient&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1e293b&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;imageBuffer&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Save or serve the image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd love feedback on the API design, the templates, and anything else. You can find me on Twitter at &lt;a href="https://twitter.com/corbanware" rel="noopener noreferrer"&gt;@corbanware&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>api</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
