<?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: dziulatex</title>
    <description>The latest articles on DEV Community by dziulatex (@dziulatex).</description>
    <link>https://dev.to/dziulatex</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%2Fuser%2Fprofile_image%2F3583307%2Fc20d548f-14e2-41de-9379-6a5d9538b97a.png</url>
      <title>DEV Community: dziulatex</title>
      <link>https://dev.to/dziulatex</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dziulatex"/>
    <language>en</language>
    <item>
      <title>og:image not showing? How to debug it and bust each cache</title>
      <dc:creator>dziulatex</dc:creator>
      <pubDate>Mon, 02 Nov 2026 10:42:28 +0000</pubDate>
      <link>https://dev.to/dziulatex/ogimage-not-showing-how-to-debug-it-and-bust-each-cache-2fmc</link>
      <guid>https://dev.to/dziulatex/ogimage-not-showing-how-to-debug-it-and-bust-each-cache-2fmc</guid>
      <description>&lt;p&gt;When a shared link shows no image — or yesterday's image — the cause is almost always one of eight things, and the platform's own debugger will tell you which in a minute. The second half of the problem is caching: every platform keeps its own copy of your preview, and each one is refreshed differently. This guide covers both, in the order you should check them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight usual causes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Relative URL.&lt;/strong&gt; &lt;code&gt;content="/og/post.png"&lt;/code&gt; works in a browser and fails in every crawler. &lt;code&gt;og:image&lt;/code&gt; must be an absolute &lt;code&gt;https://&lt;/code&gt; URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The crawler cannot fetch the image.&lt;/strong&gt; A login, a firewall, bot protection (Cloudflare's bot modes, security plugins), hotlink protection or a &lt;code&gt;robots.txt&lt;/code&gt; rule blocks &lt;code&gt;facebookexternalhit&lt;/code&gt;, &lt;code&gt;LinkedInBot&lt;/code&gt; or &lt;code&gt;Twitterbot&lt;/code&gt;. Note that Slack &lt;a href="https://api.slack.com/robots" rel="noopener noreferrer"&gt;does not honour robots.txt&lt;/a&gt; for unfurls, so "works in Slack, not on LinkedIn" often points here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong size or file.&lt;/strong&gt; Under 200×200 Facebook ignores the image; over 8 MB it rejects it (&lt;a href="https://developers.facebook.com/docs/sharing/webmasters/images" rel="noopener noreferrer"&gt;Meta&lt;/a&gt;). LinkedIn wants at least 1200×627, at most 5 MB, and lists only JPG, PNG and GIF (&lt;a href="https://www.linkedin.com/help/linkedin/answer/a521928" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;) — a WebP image can fail there while working elsewhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tags are added by JavaScript.&lt;/strong&gt; Preview crawlers read the server's HTML; a React or Vue app that sets meta tags after load shows them nothing. Render the tags on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two &lt;code&gt;og:image&lt;/code&gt; tags.&lt;/strong&gt; A theme and an SEO plugin both print one; the crawler takes the first, which is often the wrong one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own cache serves old HTML.&lt;/strong&gt; A page cache or CDN still returns the page from before you added the tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The image URL redirects badly or returns the wrong type.&lt;/strong&gt; Long redirect chains, a &lt;code&gt;200&lt;/code&gt; with an HTML error page, or &lt;code&gt;Content-Type: text/plain&lt;/code&gt; on a PNG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The platform's cache.&lt;/strong&gt; Everything is fixed, but the platform shows what it fetched last week.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: see what the crawler sees
&lt;/h2&gt;

&lt;p&gt;Fetch the page the way a crawler does and look at the tags:&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;-sL&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"facebookexternalhit/1.1"&lt;/span&gt; https://example.com/blog/post &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eio&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta[^&amp;gt;]+(og:|twitter:)[^&amp;gt;]+&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the output has no &lt;code&gt;og:image&lt;/code&gt;, the problem is in your HTML (causes 1, 4, 5, 6). If it does, check the image itself:&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;-sIL&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"facebookexternalhit/1.1"&lt;/span&gt; https://example.com/og/post.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want a final &lt;code&gt;200&lt;/code&gt;, &lt;code&gt;Content-Type: image/png&lt;/code&gt; or &lt;code&gt;image/jpeg&lt;/code&gt;, and a &lt;code&gt;Content-Length&lt;/code&gt; well under 5 MB. A &lt;code&gt;403&lt;/code&gt; here is cause 2; a &lt;code&gt;text/html&lt;/code&gt; content type is cause 7.&lt;/p&gt;

&lt;p&gt;Our free &lt;a href="https://shareglint.com/en/tools/og-checker" rel="noopener noreferrer"&gt;OG Checker&lt;/a&gt; fetches the page the way Facebook's crawler does, lists what is missing and previews the card on X, LinkedIn, Facebook, Slack and Discord; it can also scan a sitemap for pages without tags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: ask the 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;Tool&lt;/th&gt;
&lt;th&gt;Refreshes the cache?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Facebook, Messenger&lt;/td&gt;
&lt;td&gt;&lt;a href="https://developers.facebook.com/tools/debug/" rel="noopener noreferrer"&gt;Sharing Debugger&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Yes — &lt;em&gt;Scrape Again&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.linkedin.com/post-inspector/" rel="noopener noreferrer"&gt;Post Inspector&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Yes, for new posts (&lt;a href="https://www.linkedin.com/help/linkedin/answer/a6233775" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;Paste the link into the post composer (no need to post)&lt;/td&gt;
&lt;td&gt;No — re-crawled about weekly; change the URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack&lt;/td&gt;
&lt;td&gt;None needed&lt;/td&gt;
&lt;td&gt;Expires after about 30 minutes (&lt;a href="https://api.slack.com/robots" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discord&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Expires after about 30 minutes (&lt;a href="https://docs.discord.com/developers/link-previews/overview" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;); or add &lt;code&gt;?v=2&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram&lt;/td&gt;
&lt;td&gt;&lt;a href="https://t.me/WebpageBot" rel="noopener noreferrer"&gt;@WebpageBot&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Yes, for new messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;No — change the URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Facebook debugger is the most useful diagnostic even if you do not care about Facebook: it shows the exact image it chose, its dimensions, the HTTP response code it got from your server, and a warning when the image could not be downloaded or is too small. Without a manual scrape Facebook refreshes a URL's data about every 30 days; the Graph API can trigger a re-scrape (&lt;code&gt;POST /?id={url}&amp;amp;scrape=true&lt;/code&gt;), which helps when you publish many pages at once (&lt;a href="https://developers.facebook.com/documentation/sharing/webmasters/getting-started/change-link.md" rel="noopener noreferrer"&gt;Meta&lt;/a&gt;). Existing shares keep the data they were created with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: bust the cache properly
&lt;/h2&gt;

&lt;p&gt;Platforms cache two things: the &lt;strong&gt;page&lt;/strong&gt; (its tags) and the &lt;strong&gt;image&lt;/strong&gt; (the file behind the URL). Refreshing the page in a debugger does not always refresh the image — Meta's own advice is to "use a new URL for the new image" (&lt;a href="https://developers.facebook.com/docs/sharing/webmasters/images" rel="noopener noreferrer"&gt;Meta&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;So when you change an image, change its 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="c"&gt;&amp;lt;!-- before --&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://example.com/og/post.png"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- after: a new file name or a version parameter --&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://example.com/og/post-v2.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;Generated images get this for free if the URL depends on the content: a hash of the title in the file name, or a signed URL whose query string contains the title. Change the title and the URL changes with it. Shareglint's signed &lt;code&gt;/i/&lt;/code&gt; URLs and auto-OG &lt;code&gt;/u/&lt;/code&gt; URLs work this way — and an identical image is served from cache without a new render.&lt;/p&gt;

&lt;p&gt;For platforms with no refresh tool (X, Discord, WhatsApp), the page URL is the cache key too. X re-crawls cards roughly every seven days and suggests sharing a changed URL to see a fix sooner (&lt;a href="https://web.archive.org/web/2024/https://developer.x.com/en/docs/twitter-for-websites/cards/guides/troubleshooting-cards" rel="noopener noreferrer"&gt;archived X docs&lt;/a&gt;). New posts pick up a new preview once the cached one expires; to force it now, share a URL the platform has not seen, such as &lt;code&gt;https://example.com/blog/post?v=2&lt;/code&gt;. Do not count on previews inside messages and posts that were already sent changing; on most platforms they keep what was fetched at the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework-specific traps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js:&lt;/strong&gt; since v15.2 &lt;code&gt;generateMetadata&lt;/code&gt; can stream its tags after the page body, but Next.js keeps them in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; for "HTML-limited bots" such as &lt;code&gt;facebookexternalhit&lt;/code&gt;, detected by user agent (&lt;a href="https://nextjs.org/docs/app/api-reference/functions/generate-metadata#streaming-metadata" rel="noopener noreferrer"&gt;Next.js docs&lt;/a&gt;). If you serve crawlers through a custom proxy or rewrite the user agent, check that they still get blocking metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-page apps (Vite, CRA, Vue CLI):&lt;/strong&gt; prerender the routes you want shared, or add server-side rendering for meta tags; client-side helmet libraries do not help crawlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPress:&lt;/strong&gt; purge the page cache after changing SEO plugin settings; see &lt;a href="https://shareglint.com/en/guides/wordpress-og-images-without-code" rel="noopener noreferrer"&gt;automatic OG images in WordPress&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static sites:&lt;/strong&gt; a relative image path in front matter is the classic bug; see &lt;a href="https://shareglint.com/en/guides/og-images-static-sites-hugo-jekyll-astro" rel="noopener noreferrer"&gt;OG images for Hugo, Jekyll and Astro&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When the image is generated on request
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;og:image&lt;/code&gt; points at an image that is rendered when first requested (a signed URL, &lt;code&gt;next/og&lt;/code&gt;, your own Puppeteer endpoint), the first crawler visit pays for the render. Crawlers have short timeouts; a cold headless Chrome that needs five seconds can make the first share imageless, and the platform caches that result. Two fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Warm it:&lt;/strong&gt; after publishing, request the image once yourself (or paste the URL into the Facebook debugger) before anyone shares it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render ahead of time:&lt;/strong&gt; generate the image when the post is saved and store the URL, as shown in the &lt;a href="https://shareglint.com/en/guides/dynamic-og-images-laravel" rel="noopener noreferrer"&gt;Laravel&lt;/a&gt;, &lt;a href="https://shareglint.com/en/guides/dynamic-og-images-rails" rel="noopener noreferrer"&gt;Rails&lt;/a&gt; and &lt;a href="https://shareglint.com/en/guides/dynamic-og-images-django" rel="noopener noreferrer"&gt;Django&lt;/a&gt; guides.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A 60-second checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;og:image&lt;/code&gt; is absolute, &lt;code&gt;https://&lt;/code&gt;, and appears once in the server HTML.&lt;/li&gt;
&lt;li&gt;[ ] The image returns &lt;code&gt;200&lt;/code&gt;, an image content type, 1200×630, under 5 MB, JPEG or PNG.&lt;/li&gt;
&lt;li&gt;[ ] Crawlers are not blocked by a login, firewall, bot protection or hotlink rule.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;og:image:width&lt;/code&gt;, &lt;code&gt;og:image:height&lt;/code&gt; and &lt;code&gt;twitter:card&lt;/code&gt; are present.&lt;/li&gt;
&lt;li&gt;[ ] After a change: new image URL, page cache purged, Facebook &lt;em&gt;Scrape Again&lt;/em&gt;, LinkedIn Post Inspector.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why is my og:image not showing on Facebook?
&lt;/h3&gt;

&lt;p&gt;Usually one of: the image URL is relative, the image is smaller than 200×200 or larger than 8 MB, the crawler is blocked by a firewall or login, or Facebook cached an older version of the page. Paste the URL into the Sharing Debugger: it lists the image it found and any warnings, and Scrape Again refreshes the cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I force LinkedIn to refresh a link preview?
&lt;/h3&gt;

&lt;p&gt;Inspect the URL in LinkedIn's Post Inspector, which fetches the page again; new posts then use the refreshed preview. Posts that were already published keep the old one.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I refresh a Telegram or WhatsApp link preview?
&lt;/h3&gt;

&lt;p&gt;Telegram has an official bot, @WebpageBot: send it the URL and it updates the cached preview for new messages. WhatsApp has no refresh tool; change the image URL (for example add ?v=2) and, if needed, the page URL you share.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do social media crawlers run JavaScript?
&lt;/h3&gt;

&lt;p&gt;Link-preview crawlers such as facebookexternalhit, LinkedInBot, Twitterbot and Slackbot read the HTML the server returns and do not wait for client-side JavaScript. Meta tags added by a single-page app after load are invisible to them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://shareglint.com/en/guides/og-image-not-showing" rel="noopener noreferrer"&gt;Shareglint&lt;/a&gt;. Disclosure: I build Shareglint.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>html</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Next.js OG images without the @vercel/og limits</title>
      <dc:creator>dziulatex</dc:creator>
      <pubDate>Mon, 02 Nov 2026 10:40:29 +0000</pubDate>
      <link>https://dev.to/dziulatex/nextjs-og-images-without-the-vercelog-limits-5f61</link>
      <guid>https://dev.to/dziulatex/nextjs-og-images-without-the-vercelog-limits-5f61</guid>
      <description>&lt;p&gt;&lt;strong&gt;next/og&lt;/strong&gt; (the &lt;code&gt;ImageResponse&lt;/code&gt; API, built on &lt;code&gt;@vercel/og&lt;/code&gt;) is the default way to make Open Graph images in Next.js, and for a title on a gradient it is excellent. It is not a browser, though: it converts JSX to SVG with &lt;a href="https://github.com/vercel/satori" rel="noopener noreferrer"&gt;Satori&lt;/a&gt; and rasterises it with Resvg. When your card needs CSS grid, your real stylesheet, woff2 fonts or more than 500 KB of assets, the fix is not to fight Satori — it is to render the card with real Chrome and put the resulting URL in &lt;code&gt;generateMetadata&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guide lists the limits precisely, shows when they bite, and gives a drop-in replacement that works on the Node.js and Edge runtimes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits, from the Next.js docs
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://nextjs.org/docs/app/api-reference/functions/image-response" rel="noopener noreferrer"&gt;ImageResponse reference&lt;/a&gt; states them plainly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only flexbox and a subset of CSS.&lt;/strong&gt; "Advanced layouts (e.g. &lt;code&gt;display: grid&lt;/code&gt;) will not work." The full list of supported properties is in &lt;a href="https://github.com/vercel/satori#css" rel="noopener noreferrer"&gt;Satori's README&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;500 KB bundle limit&lt;/strong&gt;, counting your JSX, CSS, fonts, images and other assets together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts: &lt;code&gt;ttf&lt;/code&gt;, &lt;code&gt;otf&lt;/code&gt; and &lt;code&gt;woff&lt;/code&gt; only&lt;/strong&gt; — no &lt;code&gt;woff2&lt;/code&gt;, which is what most font services and &lt;code&gt;next/font&lt;/code&gt; hand you.&lt;/li&gt;
&lt;li&gt;Default output is &lt;strong&gt;1200×630 PNG&lt;/strong&gt;, which is the right size for Open Graph.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice people hit these as: a two-column card that needs grid; a brand font that only ships as woff2 (convert it, or it silently falls back); two weights of a CJK font that blow past 500 KB on their own; a logo as SVG with filters Resvg does not support; and the general cost of re-implementing a design in inline styles because the site's CSS is not available.&lt;/p&gt;

&lt;p&gt;A note if you read Satori's own README: recent standalone Satori releases added CSS grid and WOFF2, but the &lt;code&gt;@vercel/og&lt;/code&gt; that Next.js bundles (1.0.x in Next 16, built on an older Satori) does not have them — the Next.js limits above are the ones that apply to &lt;code&gt;next/og&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;None of this is a bug. Satori trades completeness for running anywhere in milliseconds. If your card fits inside it, keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option A: keep next/og, but stay inside the lines
&lt;/h2&gt;

&lt;p&gt;If the limits are only fonts and size, fix those and stay with &lt;code&gt;ImageResponse&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/opengraph-image.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="s1"&gt;next/og&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&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;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ttf/otf parse fastest; subset the font to Latin to stay far below 500 KB.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assets/Inter-Bold-latin.ttf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="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;getPost&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;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&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;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;space-between&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;72&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;#0b0a09&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;#fff&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="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;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;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.1&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;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="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;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.7&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;yourblog.com&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;&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;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Inter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&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;inter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&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;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normal&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subsetting fonts (for example with &lt;code&gt;pyftsubset&lt;/code&gt; from fonttools) is the single most effective fix for the 500 KB limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option B: real Chrome, as a URL in generateMetadata
&lt;/h2&gt;

&lt;p&gt;When you need grid, your stylesheet, woff2, emoji fonts or complex scripts, render the card from HTML and CSS in a real browser. Running Chrome inside a serverless function is possible (a trimmed Chromium build such as &lt;code&gt;@sparticuz/chromium&lt;/code&gt; with &lt;code&gt;puppeteer-core&lt;/code&gt;), but cold starts of several seconds and memory limits make it fragile — and a link-preview crawler will not wait.&lt;/p&gt;

&lt;p&gt;The lighter option is a &lt;strong&gt;signed image URL&lt;/strong&gt;: you compute it in &lt;code&gt;generateMetadata&lt;/code&gt; (no network call, no API key on the page), and the image service renders it on the first request and serves the cached file afterwards. Below is Shareglint's URL format (disclosure: I build Shareglint) with its built-in &lt;code&gt;blog-og&lt;/code&gt; template — swap in your own HTML template's slug to use your own design. Web Crypto makes the same code run on Node.js and Edge:&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;// lib/og.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;!'()*&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&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;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&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;ogImageUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&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;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&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="p"&gt;,&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/i/&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;OG_ACCOUNT_ID&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="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;enc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&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;enc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;&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;key&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&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;OG_SIGNING_SECRET&lt;/span&gt;&lt;span class="o"&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sign&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mac&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&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="nx"&gt;path&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="nx"&gt;query&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`https://shareglint.com&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&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="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;s=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="s1"&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;ogImageUrl&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;@/lib/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="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="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="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="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;getPost&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;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;image&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;ogImageUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog-og&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;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;author&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;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;site&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yourblog.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;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;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&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="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="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="s1"&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="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="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;encodeURIComponent&lt;/code&gt; leaves &lt;code&gt;! ' ( ) *&lt;/code&gt; unescaped, which RFC 3986 does not — the &lt;code&gt;replace&lt;/code&gt; fixes that so the signature matches. We checked this function against the test vector in the &lt;a href="https://shareglint.com/en/docs#signed" rel="noopener noreferrer"&gt;API docs&lt;/a&gt;. &lt;code&gt;OG_SIGNING_SECRET&lt;/code&gt; has no &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix, so Next.js never inlines it into client bundles; only the signature ends up in the HTML.&lt;/p&gt;

&lt;p&gt;Two Next.js details to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File conventions win.&lt;/strong&gt; If a route segment has an &lt;code&gt;opengraph-image.tsx&lt;/code&gt; or &lt;code&gt;.png&lt;/code&gt;, Next.js uses it instead of &lt;code&gt;openGraph.images&lt;/code&gt; from &lt;code&gt;generateMetadata&lt;/code&gt; — the &lt;a href="https://nextjs.org/docs/app/api-reference/functions/generate-metadata" rel="noopener noreferrer"&gt;generateMetadata docs&lt;/a&gt; say file-based metadata "has the higher priority and will override" both. Delete the file when you switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Absolute URLs.&lt;/strong&gt; A signed URL is already absolute. If you keep any relative image paths, set &lt;code&gt;metadataBase&lt;/code&gt; in the root layout or Next.js cannot build the full URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Option C: render at build time for static exports
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;output: 'export'&lt;/code&gt; there is no server at request time. A signed URL still works — it is a string computed during the build. If you would rather ship files, call the API from a build script and store the returned URLs:&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;// scripts/og-images.ts — run before `next build`&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://shareglint.com/v1/images?wait=true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;OG_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="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;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;blog-og&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;modifications&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;site&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yourblog.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="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;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// same fields again = same file, no extra render&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Title, logo, gradient; fonts under 500 KB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;next/og&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS grid, your own stylesheet, woff2, complex scripts&lt;/td&gt;
&lt;td&gt;Real Chrome — signed URL (B)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No server at all (&lt;code&gt;output: 'export'&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Signed URL computed at build, or a build script (C)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One generic card for every page&lt;/td&gt;
&lt;td&gt;A static &lt;code&gt;opengraph-image.png&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Whichever you pick, verify the result where it is seen: paste a production URL into &lt;a href="https://www.linkedin.com/post-inspector/" rel="noopener noreferrer"&gt;LinkedIn's Post Inspector&lt;/a&gt; and &lt;a href="https://developers.facebook.com/tools/debug/" rel="noopener noreferrer"&gt;Facebook's Sharing Debugger&lt;/a&gt; — &lt;code&gt;localhost&lt;/code&gt; URLs cannot be fetched by either.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are the limits of next/og ImageResponse?
&lt;/h3&gt;

&lt;p&gt;According to the Next.js docs: only flexbox and a subset of CSS (no display: grid), a 500KB bundle limit including JSX, CSS, fonts and images, and only ttf, otf and woff fonts. It renders with Satori and Resvg, not a browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Tailwind or my existing React components in an OG image?
&lt;/h3&gt;

&lt;p&gt;Only partly with next/og: Satori accepts inline styles and a tw prop, but not your stylesheet, CSS grid or arbitrary components. A renderer that runs real Chrome takes ordinary HTML and CSS, so the card can share your design system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does opengraph-image.tsx override generateMetadata?
&lt;/h3&gt;

&lt;p&gt;Yes. Next.js gives file-based metadata higher priority than config-based metadata, so an opengraph-image file in a route segment replaces the openGraph.images you return from generateMetadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need the Edge runtime for OG images?
&lt;/h3&gt;

&lt;p&gt;No. ImageResponse works on both the Node.js and Edge runtimes, and a signed image URL is just a string you compute — Web Crypto makes the same code work on either.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://shareglint.com/en/guides/nextjs-og-images-without-vercel-og-limits" rel="noopener noreferrer"&gt;Shareglint&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
    <item>
      <title>What is an Open Graph image? Tags, platforms and fixes</title>
      <dc:creator>dziulatex</dc:creator>
      <pubDate>Mon, 02 Nov 2026 10:36:23 +0000</pubDate>
      <link>https://dev.to/dziulatex/what-is-an-open-graph-image-tags-platforms-and-fixes-4d3b</link>
      <guid>https://dev.to/dziulatex/what-is-an-open-graph-image-tags-platforms-and-fixes-4d3b</guid>
      <description>&lt;p&gt;An &lt;strong&gt;Open Graph image&lt;/strong&gt; (OG image) is the picture a link shows when it is shared on LinkedIn, Facebook, X, Slack, Discord, WhatsApp or iMessage. You set it with one tag in the page's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; — &lt;code&gt;&amp;lt;meta property="og:image" content="https://example.com/og/post.png"&amp;gt;&lt;/code&gt; — and each platform's crawler downloads it the first time the link is shared. The size that works everywhere is &lt;strong&gt;1200×630 pixels&lt;/strong&gt;, as JPEG or PNG under 600 KB; the &lt;a href="https://shareglint.com/en/og-image-size" rel="noopener noreferrer"&gt;per-platform table&lt;/a&gt; has every documented limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimal tag set to copy
&lt;/h2&gt;

&lt;p&gt;Paste this into the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of every page, with that page's own values:&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;"How we cut our image pipeline to 900 ms"&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;"Moving rendering off the request path and caching by content hash."&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:type"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"article"&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://example.com/blog/image-pipeline"&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://example.com/og/image-pipeline.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:image:alt"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Title card: How we cut our image pipeline to 900 ms"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why each line is there:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:type&lt;/code&gt;, &lt;code&gt;og:url&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The four properties the &lt;a href="https://ogp.me/" rel="noopener noreferrer"&gt;Open Graph protocol&lt;/a&gt; calls required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;og:description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The line under the title; LinkedIn and WhatsApp list it as required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;og:image:width&lt;/code&gt;, &lt;code&gt;og:image:height&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Lets Facebook "render the image immediately without having to asynchronously download and process it" (&lt;a href="https://developers.facebook.com/docs/sharing/webmasters/images" rel="noopener noreferrer"&gt;Meta&lt;/a&gt;); Discord otherwise downloads the image just to measure it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;og:image:alt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A description of the image, "not a caption"; ogp.me says a page with &lt;code&gt;og:image&lt;/code&gt; should set it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;twitter:card&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Chooses X's card layout; &lt;code&gt;summary_large_image&lt;/code&gt; is the big image above the title&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Open Graph uses &lt;code&gt;property=&lt;/code&gt;, Twitter Cards use &lt;code&gt;name=&lt;/code&gt;. Most parsers accept either, but following each spec avoids surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  How each platform reads it
&lt;/h2&gt;

&lt;p&gt;When a link is pasted, the platform fetches the page's HTML, reads the tags, then fetches the image from its own servers. Apple states that Messages runs no JavaScript, and no other platform documents running it, so treat the HTML your server sends as all they see. What each one documents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Facebook&lt;/strong&gt; (crawler &lt;code&gt;facebookexternalhit&lt;/code&gt;) lists &lt;code&gt;og:url&lt;/code&gt;, &lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt; and &lt;code&gt;og:image&lt;/code&gt; as the basic tags (&lt;a href="https://developers.facebook.com/docs/sharing/webmasters/" rel="noopener noreferrer"&gt;Meta&lt;/a&gt;), and the Open Graph properties must come before the first 1 MB of the page (&lt;a href="https://developers.facebook.com/docs/sharing/webmasters/web-crawlers" rel="noopener noreferrer"&gt;Meta crawlers&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt; says &lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt; and &lt;code&gt;og:url&lt;/code&gt; "must exist" (&lt;a href="https://www.linkedin.com/help/linkedin/answer/a521928" rel="noopener noreferrer"&gt;LinkedIn Help&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X&lt;/strong&gt; reads its own &lt;code&gt;twitter:&lt;/code&gt; tags and falls back to &lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt; and &lt;code&gt;og:image&lt;/code&gt; when one is missing. Without &lt;code&gt;twitter:card&lt;/code&gt;, "a summary card may be rendered" — the small one with a square image (&lt;a href="https://web.archive.org/web/20260116124848/https://developer.x.com/en/docs/x-for-websites/cards/overview/markup" rel="noopener noreferrer"&gt;X cards markup, archived&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack&lt;/strong&gt; (&lt;code&gt;Slackbot-LinkExpanding&lt;/code&gt;) reads oEmbed, Twitter Card and Open Graph tags and caches the result for "around 30 minutes" (&lt;a href="https://api.slack.com/robots" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;). It does not honour &lt;code&gt;robots.txt&lt;/code&gt; for unfurls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discord&lt;/strong&gt; reads &lt;code&gt;og:&lt;/code&gt; and &lt;code&gt;twitter:&lt;/code&gt; tags and &lt;code&gt;theme-color&lt;/code&gt; for the accent bar, shows a small image unless &lt;code&gt;twitter:card&lt;/code&gt; is &lt;code&gt;summary_large_image&lt;/code&gt;, and gives the page 10 seconds (&lt;a href="https://docs.discord.com/developers/link-previews/overview" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp&lt;/strong&gt; needs &lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt;, &lt;code&gt;og:url&lt;/code&gt; and an absolute &lt;code&gt;og:image&lt;/code&gt; under 600 KB, with the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; inside the first 300 KB of HTML (&lt;a href="https://developers.facebook.com/documentation/business-messaging/whatsapp/link-previews/" rel="noopener noreferrer"&gt;Meta&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iMessage&lt;/strong&gt; reads Open Graph tags, follows server redirects but not JavaScript ones, and wants images at least 900 px wide (&lt;a href="https://developer.apple.com/documentation/technotes/tn3156-create-rich-previews-for-messages" rel="noopener noreferrer"&gt;Apple TN3156&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two rules follow for developers: the tags must be in the HTML the server sends, and the image URL must be absolute and public — the crawler fetches it without your cookies.&lt;/p&gt;

&lt;h2&gt;
  
  
  og:image vs twitter:image
&lt;/h2&gt;

&lt;p&gt;X has its own tags (Twitter Cards). &lt;code&gt;twitter:card&lt;/code&gt; picks the layout, &lt;code&gt;twitter:image&lt;/code&gt; the picture, &lt;code&gt;twitter:title&lt;/code&gt; and &lt;code&gt;twitter:description&lt;/code&gt; the text. The last three fall back to their Open Graph counterparts; &lt;code&gt;twitter:card&lt;/code&gt; falls back only to the small summary card. So the usual set is &lt;code&gt;og:image&lt;/code&gt; plus one line:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;twitter:image&lt;/code&gt; only when X should get a different picture from everyone else. The large card is 2:1 and the plain &lt;code&gt;summary&lt;/code&gt; card crops the image to a square, so 1200×630 suits &lt;code&gt;summary_large_image&lt;/code&gt;, not &lt;code&gt;summary&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failures, and how to see them
&lt;/h2&gt;

&lt;p&gt;Six common causes, each with a way to see it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Relative image URL.&lt;/strong&gt; &lt;code&gt;content="/og/post.png"&lt;/code&gt; works in a browser, not everywhere else: WhatsApp asks for an absolute URL, and Discord accepts a relative one only when it starts with &lt;code&gt;/&lt;/code&gt;. Write the scheme and host every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The image answers 404, 403 or a redirect to a login page.&lt;/strong&gt; The crawler is anonymous. Open the image URL in a private window; if you see anything but the image, so does the crawler. Hotlink protection and bot-blocking firewalls cause the same symptom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tags added by JavaScript.&lt;/strong&gt; A single-page app that sets meta tags after load risks showing nothing: Messages runs no scripts, and no other platform says it does. Render the tags on the server or at build time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong size or file.&lt;/strong&gt; Under 600×315 Facebook shows a small image; under 401 px wide LinkedIn shows a thumbnail; WhatsApp asks for under 600 KB. The &lt;a href="https://shareglint.com/en/og-image-size" rel="noopener noreferrer"&gt;size reference&lt;/a&gt; lists each platform's limits and has an in-browser check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tags too far down the page.&lt;/strong&gt; Facebook reads Open Graph properties only in the first 1 MB, WhatsApp the first 300 KB. Put them near the top of &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, before large inline scripts or styles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The old preview is cached.&lt;/strong&gt; Fixing the tag does not change a card that was already fetched. Facebook caches images by URL, so give a new image a new URL and re-scrape the page in the &lt;a href="https://developers.facebook.com/tools/debug/" rel="noopener noreferrer"&gt;Sharing Debugger&lt;/a&gt;. LinkedIn's &lt;a href="https://www.linkedin.com/post-inspector/" rel="noopener noreferrer"&gt;Post Inspector&lt;/a&gt; refreshes its preview. Slack and Discord keep previews for about 30 minutes; X documents no refresh tool.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To see all of this at once, paste the page into the free &lt;a href="https://shareglint.com/en/tools/og-checker" rel="noopener noreferrer"&gt;OG Checker&lt;/a&gt;: it fetches the page like a crawler, lists missing or broken tags and previews the card on X, LinkedIn, Facebook, Slack and Discord. The &lt;a href="https://shareglint.com/en/guides/og-image-not-showing" rel="noopener noreferrer"&gt;debugging guide&lt;/a&gt; goes through each platform step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes a good OG image
&lt;/h2&gt;

&lt;p&gt;The image is shown at a few hundred pixels wide in a feed and smaller in chat apps, next to your title in plain text. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Large type, few words.&lt;/strong&gt; A title set at 60–80 px on a 1200-px canvas stays readable when scaled down; body-size text does not. Apple advises against text in preview images altogether, because Messages shows them at varying sizes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it away from the edges.&lt;/strong&gt; X's large card is 2:1, and how it crops other ratios is not documented. Keep the title and logo inside the middle 1200×600.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand consistently.&lt;/strong&gt; A logo or colour bar in the same place on every card makes your links recognisable in a busy feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small files.&lt;/strong&gt; Our own 1200×630 text cards come out at 55–60 KB as JPEG; whatever you use, stay under WhatsApp's 600 KB.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How sites produce one image per page
&lt;/h2&gt;

&lt;p&gt;A single site-wide image is fine for a homepage; for articles, products, docs pages and job posts, a per-page image with that page's title works better. The common ways to make them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;By hand&lt;/strong&gt; in a design tool — fine for a handful of pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At build time or in a job&lt;/strong&gt;, with a headless browser (Puppeteer, Playwright) or a drawing library (Pillow, GD, Satori/&lt;code&gt;next/og&lt;/code&gt;). Free per image, but you maintain the renderer, fonts and memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With an image API&lt;/strong&gt;: you keep a template and send the page's fields, from your server or as a signed URL right in the &lt;code&gt;og:image&lt;/code&gt; tag. Shareglint (disclosure: I build it) is one of these: 153 built-in 1200×630 &lt;a href="https://shareglint.com/en/templates" rel="noopener noreferrer"&gt;templates&lt;/a&gt;, your own HTML and CSS, and a Free plan of 100 images a month — the &lt;a href="https://shareglint.com/en/docs" rel="noopener noreferrer"&gt;API docs&lt;/a&gt; show the request. Others include Bannerbear, Placid and HTMLCSStoImage; see the &lt;a href="https://shareglint.com/en/guides/bannerbear-vs-placid" rel="noopener noreferrer"&gt;comparison&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With a screenshot of the page itself&lt;/strong&gt; — quick, but a desktop screenshot is the wrong shape and hard to read at thumbnail size; &lt;a href="https://shareglint.com/en/guides/screenshot-api-vs-templates" rel="noopener noreferrer"&gt;when screenshots make sense&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Framework walkthroughs: &lt;a href="https://shareglint.com/en/guides/nextjs-og-images-without-vercel-og-limits" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt;, &lt;a href="https://shareglint.com/en/guides/dynamic-og-images-laravel" rel="noopener noreferrer"&gt;Laravel&lt;/a&gt;, &lt;a href="https://shareglint.com/en/guides/dynamic-og-images-rails" rel="noopener noreferrer"&gt;Rails&lt;/a&gt;, &lt;a href="https://shareglint.com/en/guides/dynamic-og-images-django" rel="noopener noreferrer"&gt;Django&lt;/a&gt;, &lt;a href="https://shareglint.com/en/guides/wordpress-og-images-without-code" rel="noopener noreferrer"&gt;WordPress&lt;/a&gt; and &lt;a href="https://shareglint.com/en/guides/og-images-static-sites-hugo-jekyll-astro" rel="noopener noreferrer"&gt;static sites&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an OG image?
&lt;/h3&gt;

&lt;p&gt;The image set in a page's og:image meta tag. Social networks and messaging apps show it in the preview card when someone shares a link to the page, next to the title and description from og:title and og:description.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is og:image required?
&lt;/h3&gt;

&lt;p&gt;The Open Graph protocol lists og:image among its four required properties, with og:title, og:type and og:url, and LinkedIn and WhatsApp both list it among the tags a preview needs. Without it most platforms show a text-only preview.&lt;/p&gt;

&lt;h3&gt;
  
  
  What size should an OG image be?
&lt;/h3&gt;

&lt;p&gt;1200×630 pixels (1.91:1), JPEG or PNG, under 600 KB. That meets Facebook's recommendation, LinkedIn's 1200×627 minimum, X's card limits and WhatsApp's 600 KB limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between og:image and twitter:image?
&lt;/h3&gt;

&lt;p&gt;og:image comes from the Open Graph protocol and is read by almost every platform. twitter:image is X's own tag; X uses it when present and falls back to og:image when it is missing. Set twitter:card to summary_large_image too: without it X may render the small summary card.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my old OG image still show?
&lt;/h3&gt;

&lt;p&gt;Platforms cache previews. Facebook caches images by URL, so give a new image a new URL and re-scrape in the Sharing Debugger; LinkedIn's Post Inspector refreshes a preview; Slack and Discord keep one for about 30 minutes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://shareglint.com/en/guides/what-is-an-open-graph-image" rel="noopener noreferrer"&gt;Shareglint&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>html</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Built an API that translates text to Gen Z slang (works in any language)</title>
      <dc:creator>dziulatex</dc:creator>
      <pubDate>Sat, 25 Oct 2025 22:23:45 +0000</pubDate>
      <link>https://dev.to/dziulatex/built-an-api-that-translates-text-to-gen-z-slang-works-in-any-language-3le8</link>
      <guid>https://dev.to/dziulatex/built-an-api-that-translates-text-to-gen-z-slang-works-in-any-language-3le8</guid>
      <description>&lt;p&gt;Made a simple API that converts normal sentences into Gen Z slang using Claude AI.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
  "I'm having a great day" → "Today hits different fr fr, got that W energy no cap 💯"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech:&lt;/strong&gt; Symfony + Claude AI + 40-term slang dictionary&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cool part:&lt;/strong&gt; Works in multiple languages automatically - just one prompt rule made it multilingual.&lt;/p&gt;

&lt;p&gt;Built it for fun but now it's on RapidAPI. Thinking of adding "slang intensity levels" next.&lt;/p&gt;

&lt;p&gt;Drop a sentence and I'll convert it for you 👇&lt;/p&gt;

&lt;p&gt;[Link if interested: &lt;a href="https://rapidapi.com/dziulatex/api/boomer-language-to-genz-slang" rel="noopener noreferrer"&gt;https://rapidapi.com/dziulatex/api/boomer-language-to-genz-slang&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
