<?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: Hugo Bernardo Cardoso</title>
    <description>The latest articles on DEV Community by Hugo Bernardo Cardoso (@hugo_bernardocardoso_d38).</description>
    <link>https://dev.to/hugo_bernardocardoso_d38</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%2F4050021%2F0e7bcfb6-0269-4390-8e3e-5f84b22192b5.jpg</url>
      <title>DEV Community: Hugo Bernardo Cardoso</title>
      <link>https://dev.to/hugo_bernardocardoso_d38</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hugo_bernardocardoso_d38"/>
    <language>en</language>
    <item>
      <title>From Zero to Production: Building a Secure OG Image API</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Tue, 18 Aug 2026 08:22:57 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/from-zero-to-production-building-a-secure-og-image-api-3knb</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/from-zero-to-production-building-a-secure-og-image-api-3knb</guid>
      <description>&lt;h1&gt;
  
  
  From Zero to Production: Building a Secure OG Image API
&lt;/h1&gt;

&lt;p&gt;Every time you paste a link into Slack, WhatsApp, or X, a tiny war is being fought. The social platform's crawler hits your page, reads the Open Graph tags, and decides what to show. If your &lt;code&gt;og:image&lt;/code&gt; is missing, slow, or ugly, your link becomes a bare URL. No thumbnail. No headline. No click.&lt;/p&gt;

&lt;p&gt;For most developers, the fix is a static image. But static images don't scale when you're generating a unique preview for every product page, blog post, or pricing tier. You need dynamic OG images — and you need them served securely, at speed, without melting your server budget.&lt;/p&gt;

&lt;p&gt;This is the story of building exactly that: a production-grade OG image API with HMAC-signed URLs, headless Chrome rendering, and edge caching that makes the first request the only expensive one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Why Your Previews Fail
&lt;/h2&gt;

&lt;p&gt;Client-side OG image generation is a trap. Social network crawlers — LinkedIn, Twitter, Facebook — don't execute JavaScript. They fetch your URL, read the raw HTML, and move on. If your &lt;code&gt;og:image&lt;/code&gt; is generated client-side, the crawler sees nothing.&lt;/p&gt;

&lt;p&gt;Server-side generation fixes this, but introduces its own problems. Rendering a 1200×630 image on every request is expensive. A naive implementation using a headless browser per request will eat CPU, exhaust memory, and time out under load.&lt;/p&gt;

&lt;p&gt;The solution is a dedicated API that separates rendering from serving. You pay for the render once, cache the result aggressively, and let the edge handle the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Three Moving Parts
&lt;/h2&gt;

&lt;p&gt;FastOG's approach breaks down into three components that work together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A signed URL layer&lt;/strong&gt; — every request carries an HMAC-SHA256 signature so only authorized callers can trigger renders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A render service&lt;/strong&gt; — headless Chrome (via Browsershot) renders Svelte 5 templates at exactly 1200×630&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A caching layer&lt;/strong&gt; — successful renders are cached for a year with &lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The signing layer is the part most people get wrong. You might be tempted to put your API key in a header. That works for your own server-side code, but it breaks the moment a social network crawler tries to fetch the image. Crawlers don't send custom headers. They just fetch the URL.&lt;/p&gt;

&lt;p&gt;That's why signatures live in the URL itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  URL-Signed Security: The HMAC Pattern
&lt;/h2&gt;

&lt;p&gt;Here's the core endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/v1/og?key=YOUR_API_KEY&amp;amp;s=SIGNATURE&amp;amp;title=Hello&amp;amp;template=blog
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature is computed by taking the canonical sorted query string, RFC 3986-encoding it, and HMAC-ing it with your secret key. Every API key has its own secret.&lt;/p&gt;

&lt;p&gt;A minimal Node.js signer looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&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;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sign&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;secret&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;canonical&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;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&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="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="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;encodeURIComponent&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;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="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;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;canonical&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;signature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your_api_key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="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;How to Build an OG Image API&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;devblog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://fastog.com/api/v1/og?&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your_secret&lt;/span&gt;&lt;span class="dl"&gt;'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature goes in the URL because that's the only thing crawlers will faithfully transmit. It's a deliberate trade-off: you accept that signatures are visible in logs in exchange for images that work everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering: Why Satori Isn't Enough
&lt;/h2&gt;

&lt;p&gt;You can generate OG images without a browser. Libraries like Satori render JSX to SVG, then you convert to PNG. It's fast and lightweight. But it has limits: no complex CSS, no web fonts, no JavaScript.&lt;/p&gt;

&lt;p&gt;When you need 41 different templates — ecommerce cards with prices and ratings, podcast episodes with progress bars, countdown timers for launches — a real browser becomes the pragmatic choice.&lt;/p&gt;

&lt;p&gt;FastOG runs a separate Node render service that receives render jobs and drives headless Chrome via Browsershot. The service has a self-healing watchdog: a health check endpoint that auto-restarts the Docker container if it wedges. This matters because headless Chrome &lt;em&gt;will&lt;/em&gt; wedge. It's not a question of if, but when.&lt;/p&gt;

&lt;p&gt;The render service is separate from the API for a reason. If Chrome crashes, the API stays up. If the API is under load, renders queue gracefully. You don't want a browser crash taking down your whole endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credit Economics: Deduct Only on Success
&lt;/h2&gt;

&lt;p&gt;The billing model is simple: each render costs 1 credit. But the important detail is &lt;em&gt;when&lt;/em&gt; credits are deducted.&lt;/p&gt;

&lt;p&gt;Deduction happens only after a successful render, protected by an atomic lock to prevent double-spending. If the render fails — invalid parameters, render service unreachable, rate limit exceeded — you get a machine-readable error code (&lt;code&gt;insufficient_credits&lt;/code&gt;, &lt;code&gt;render_unreachable&lt;/code&gt;) and your credits stay untouched.&lt;/p&gt;

&lt;p&gt;The response headers tell you where you stand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Renders-Remaining: 42
X-FastOG-Watermark: false
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters for production systems. You can build retry logic around &lt;code&gt;502&lt;/code&gt; and &lt;code&gt;503&lt;/code&gt; responses without worrying about being charged for failed attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching: The First Request Is the Only Expensive One
&lt;/h2&gt;

&lt;p&gt;Here's the economics that make this viable: the first request for a given set of parameters triggers a render. That render costs 1 credit. Every subsequent request for the same URL — from LinkedIn's crawler, from Slack's unfurler, from a user's browser — hits the cache.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt; header tells every intermediary (CDN, browser, crawler) to hold onto the image for a year. Your cost per image approaches zero as the image gets shared more.&lt;/p&gt;

&lt;p&gt;This is the pattern that makes dynamic OG images practical at scale. You're not paying per impression; you're paying per unique image.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Zero to Production: The Checklist
&lt;/h2&gt;

&lt;p&gt;If you're building your own OG image service, here's the minimum viable architecture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign every request&lt;/strong&gt; — HMAC-SHA256 in the URL, not headers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate rendering from serving&lt;/strong&gt; — a crash in Chrome shouldn't take down your API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache aggressively&lt;/strong&gt; — immutable cache headers, long max-age&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduct credits only on success&lt;/strong&gt; — atomic locks, machine-readable errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a health check&lt;/strong&gt; — headless Chrome will crash; plan for it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;FastOG implements all of this out of the box. You get 100 free credits on signup, credit packs start at $2, and there's a free OG Image Tester that requires no account at all — you can preview templates and download a real PNG client-side before committing to anything.&lt;/p&gt;

&lt;p&gt;The free tester is the fastest way to understand what you're buying. Paste your title, pick a template, see exactly what LinkedIn will display. No signup, no credit card, no friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Dynamic OG images are a competitive advantage. A link with a well-designed preview gets clicked; a bare URL gets scrolled past. The technical challenge is making generation secure, reliable, and affordable.&lt;/p&gt;

&lt;p&gt;URL-signed HMAC authentication solves the crawler problem. A dedicated render service with a watchdog solves the reliability problem. Year-long immutable caching solves the cost problem.&lt;/p&gt;

&lt;p&gt;The result is an API where the first request costs one credit and everything after is free — and that's a trade worth making.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Implement Dynamic OG Images in Next.js, Remix, or Laravel: A Step-by-Step Guide</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Mon, 17 Aug 2026 08:25:40 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/how-to-implement-dynamic-og-images-in-nextjs-remix-or-laravel-a-step-by-step-guide-4on7</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/how-to-implement-dynamic-og-images-in-nextjs-remix-or-laravel-a-step-by-step-guide-4on7</guid>
      <description>&lt;h1&gt;
  
  
  How to Implement Dynamic OG Images in Next.js, Remix, or Laravel: A Step-by-Step Guide
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Why Your Static OG Image Is Costing You Clicks
&lt;/h2&gt;

&lt;p&gt;When you share a link on LinkedIn, X, Slack, WhatsApp, or Facebook, the platform crawls your page and pulls the &lt;code&gt;og:image&lt;/code&gt; meta tag. If that image is static — the same generic banner for every URL — you're leaving clicks on the table.&lt;/p&gt;

&lt;p&gt;Here's the problem: a static OG image says nothing about &lt;em&gt;what&lt;/em&gt; the reader will get. "How to Implement Dynamic OG Images" with a generic gradient tells me nothing. "How to Implement Dynamic OG Images — 7 Steps, 15 Minutes, Zero Server Setup" tells me everything. The latter gets the click.&lt;/p&gt;

&lt;p&gt;Dynamic OG images — generated per-URL with the actual title, author, price, or rating baked in — convert better because they set accurate expectations. The crawler sees a preview that matches the content, so the person on the other end knows exactly what they're opening.&lt;/p&gt;

&lt;p&gt;But here's the catch: &lt;strong&gt;social crawlers don't execute JavaScript&lt;/strong&gt;. Client-side rendering fails silently. WhatsApp, Twitter, and LinkedIn all fetch your URL server-side and read the raw HTML. If your OG image is generated client-side, you get a blank preview or a broken image.&lt;/p&gt;

&lt;p&gt;The fix is a server-side endpoint that returns a real image. FastOG does exactly this: you call one URL with query parameters, and you get back a ready-to-share 1200×630 PNG, JPEG, or WebP — rendered server-side by headless Chrome, not assembled client-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites: What You Need Before You Start
&lt;/h2&gt;

&lt;p&gt;Before you wire anything up, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A FastOG account&lt;/strong&gt; — you get 100 free credits on signup, no credit card required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your API key and secret&lt;/strong&gt; — found in your dashboard; each key has its own HMAC secret&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A framework&lt;/strong&gt; — Next.js (App or Pages Router), Remix, or Laravel; the pattern is identical across all three&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A template ID&lt;/strong&gt; — FastOG ships 41 server-side Svelte 5 templates (&lt;code&gt;blog&lt;/code&gt;, &lt;code&gt;product&lt;/code&gt;, &lt;code&gt;pricing&lt;/code&gt;, &lt;code&gt;saaslaunch&lt;/code&gt;, &lt;code&gt;stats&lt;/code&gt;, &lt;code&gt;bento&lt;/code&gt;, &lt;code&gt;comparison&lt;/code&gt;, &lt;code&gt;countdown&lt;/code&gt;, &lt;code&gt;promo&lt;/code&gt;, &lt;code&gt;ecommerce&lt;/code&gt;, &lt;code&gt;game&lt;/code&gt;, &lt;code&gt;news&lt;/code&gt;, &lt;code&gt;podcast&lt;/code&gt;, &lt;code&gt;video&lt;/code&gt;, &lt;code&gt;devblog&lt;/code&gt;, &lt;code&gt;codepost&lt;/code&gt;, and more)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core endpoint is &lt;code&gt;GET /api/v1/og&lt;/code&gt;. Each render costs 1 credit, and the image is cached for a year (&lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt;), so everyone after the first request gets it for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set Up Your Dynamic Image Endpoint
&lt;/h2&gt;

&lt;p&gt;The pattern is the same in every framework: accept the incoming request, build the OG image URL with your parameters, sign it, and return it as the &lt;code&gt;og:image&lt;/code&gt; meta tag.&lt;/p&gt;

&lt;p&gt;Here's the canonical flow in &lt;strong&gt;Laravel&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/web.php&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/post/{slug}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$slug&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;firstOrFail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$ogUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://api.fastog.com/api/v1/og?'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;http_build_query&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'template'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'blog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'subtitle'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'author'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;author_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.fastog.key'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'s'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sign_og_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// see Step 4&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'post'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ogImage'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ogUrl&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;In &lt;strong&gt;Next.js App Router&lt;/strong&gt;, you'd do the same inside a &lt;code&gt;generateMetadata&lt;/code&gt; function:&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/post/[slug]/page.tsx&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="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="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;ogUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildOgUrl&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&lt;/span&gt;&lt;span class="dl"&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;subtitle&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;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_name&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="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;ogUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in &lt;strong&gt;Remix&lt;/strong&gt;, inside a &lt;code&gt;loader&lt;/code&gt; or &lt;code&gt;meta&lt;/code&gt; export:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MetaFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;{&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;data&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;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;og: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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&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;The key insight: &lt;strong&gt;the OG image URL is just a URL&lt;/strong&gt;. Any framework that can emit a &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag can use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Design Your Template and Pass Data Safely
&lt;/h2&gt;

&lt;p&gt;FastOG renders templates server-side with Svelte 5 at exactly 1200×630 using headless Chrome (Browsershot) in a separate Node render service. You don't touch the rendering pipeline — you just pass data.&lt;/p&gt;

&lt;p&gt;The available query parameters are: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;subtitle&lt;/code&gt;, &lt;code&gt;template&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;original_price&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt;, &lt;code&gt;features&lt;/code&gt;, &lt;code&gt;badge&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, and more, depending on the template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security rule: treat the OG URL as untrusted input.&lt;/strong&gt; Never pass raw user content without encoding. FastOG signs the canonical sorted query with RFC 3986 encoding, so the signature covers every parameter. If you're building the URL yourself, use &lt;code&gt;URLSearchParams&lt;/code&gt; or &lt;code&gt;http_build_query&lt;/code&gt; — don't hand-concatenate strings.&lt;/p&gt;

&lt;p&gt;A safe pattern:&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;function&lt;/span&gt; &lt;span class="nf"&gt;buildOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;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="kr"&gt;string&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;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="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="na"&gt;key&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;FASTOG_KEY&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="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// HMAC signature&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`https://api.fastog.com/api/v1/og?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;search&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="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;h2&gt;
  
  
  Step 3: Handle Caching and Edge Storage
&lt;/h2&gt;

&lt;p&gt;This is where FastOG's economics get interesting. Every render costs 1 credit, but the image is cached for a year. The first request for a given URL pays the credit; everyone after that gets the cached copy for free.&lt;/p&gt;

&lt;p&gt;The response includes &lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt;. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Social crawlers&lt;/strong&gt; (LinkedIn, Twitter, Facebook) hit the cached copy — fast, no credit cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your users&lt;/strong&gt; sharing the link also hit the cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You&lt;/strong&gt; don't pay for repeat renders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're generating OG images for a high-traffic site, this is the difference between $0.0014 per image and effectively $0 after the first render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add HMAC Signing for Security
&lt;/h2&gt;

&lt;p&gt;This is the part most tutorials skip, and it's the one that matters. FastOG requires every request to carry &lt;code&gt;key&lt;/code&gt; + &lt;code&gt;s&lt;/code&gt; (signature). The signature lives in the URL, not in headers, because social network crawlers don't send custom headers.&lt;/p&gt;

&lt;p&gt;The signing algorithm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take all query parameters except &lt;code&gt;s&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sort them alphabetically by key&lt;/li&gt;
&lt;li&gt;RFC 3986-encode each key and value&lt;/li&gt;
&lt;li&gt;Concatenate as &lt;code&gt;key=value&amp;amp;key=value&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;HMAC-SHA256 the result with your API secret&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's a working implementation in &lt;strong&gt;Node.js&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&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;crypto&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;function&lt;/span&gt; &lt;span class="nf"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;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="nx"&gt;secret&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="kr"&gt;string&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;sorted&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;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&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="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;encodeURIComponent&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;encodeURIComponent&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;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="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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in &lt;strong&gt;PHP&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;ksort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;http_build_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PHP_QUERY_RFC3986&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;hash_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$secret&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;Why bother? Because without signing, anyone can burn your credits by hitting your endpoint with arbitrary parameters. With signing, only requests you authorized render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Test Across All Major Platforms
&lt;/h2&gt;

&lt;p&gt;Different platforms cache OG images differently. Here's what to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: notoriously aggressive caching. Use their Post Inspector to force a refresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X (Twitter)&lt;/strong&gt;: use the Card Validator. It shows you exactly what the crawler sees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp&lt;/strong&gt;: the most frustrating — it caches aggressively and has no public debugger. Append &lt;code&gt;?v=2&lt;/code&gt; to your URL to bust the cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facebook&lt;/strong&gt;: the Sharing Debugger lets you scrape fresh and see errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FastOG also has a free OG Image Tester — no signup required — that previews your image client-side and downloads a real PNG. It's the fastest way to sanity-check a template before you deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Go Live with Monitoring and Analytics
&lt;/h2&gt;

&lt;p&gt;Once you're live, watch these signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X-Renders-Remaining&lt;/code&gt;&lt;/strong&gt; header on every response — tells you your credit balance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X-FastOG-Watermark&lt;/code&gt;&lt;/strong&gt; header — confirms whether the image is watermarked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error codes&lt;/strong&gt; — &lt;code&gt;402&lt;/code&gt; (insufficient credits), &lt;code&gt;422&lt;/code&gt; (validation error), &lt;code&gt;502&lt;/code&gt; (render service unreachable), &lt;code&gt;503&lt;/code&gt; (service unavailable). Errors cost nothing — deduction happens only after a successful render, with an atomic lock preventing double-spend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you see a spike in &lt;code&gt;502&lt;/code&gt;s, the render service may have wedged. FastOG runs a self-healing watchdog (&lt;code&gt;og:health&lt;/code&gt;) that auto-restarts the Docker container, but you should still monitor your error rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues: Empty Previews, Caching, and Quotas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"My preview is blank on WhatsApp but works on X"&lt;/strong&gt; — WhatsApp caches aggressively and doesn't honor cache-busting headers. Append a query param (&lt;code&gt;?v=timestamp&lt;/code&gt;) to force a fresh fetch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The image is stale on LinkedIn"&lt;/strong&gt; — Use LinkedIn's Post Inspector to force a re-scrape. It's the only reliable way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'm out of credits"&lt;/strong&gt; — Check &lt;code&gt;X-Renders-Remaining&lt;/code&gt;. Cached images are free, so the fix is usually to ensure your cache headers are being respected. If you're genuinely out, credit packs start at $2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"My signature doesn't validate"&lt;/strong&gt; — The most common cause is encoding mismatch. Make sure you're using RFC 3986 encoding (not &lt;code&gt;encodeURIComponent&lt;/code&gt;'s default) and that your parameters are sorted before signing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'm getting 422s"&lt;/strong&gt; — You're passing an invalid parameter or template name. Check the template list and your parameter names against the API reference.&lt;/p&gt;




&lt;p&gt;Dynamic OG images are a small change with an outsized impact on click-through. The pattern is identical whether you're on Next.js, Remix, or Laravel: build a signed URL, emit it as &lt;code&gt;og:image&lt;/code&gt;, and let the server-side renderer do the work. FastOG handles the rendering, caching, and security — you just ship the meta tag.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Android Strings XML Translation Online: The No-Login Tool That Preserves Your Variables</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:56:57 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/android-strings-xml-translation-online-the-no-login-tool-that-preserves-your-variables-2hcc</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/android-strings-xml-translation-online-the-no-login-tool-that-preserves-your-variables-2hcc</guid>
      <description>&lt;h1&gt;
  
  
  Android Strings XML Translation Online: The No-Login Tool That Preserves Your Variables
&lt;/h1&gt;

&lt;p&gt;You're solo. You're shipping in weeks. Your &lt;code&gt;strings.xml&lt;/code&gt; file has 1,200 lines, and 14 of them contain &lt;code&gt;%1$s&lt;/code&gt; or &lt;code&gt;{player_name}&lt;/code&gt; placeholders that will crash your app if a translator touches them.&lt;/p&gt;

&lt;p&gt;Agencies want $2,000 and three weeks. ChatGPT mangles your variable tags, skips long strings, and you're stuck fixing broken JSON at 2 AM.&lt;/p&gt;

&lt;p&gt;LocaFile AI is the android strings XML translation online tool built for exactly this moment. No login. No subscription. No per-word haggling. You upload your file, pay a flat fee, and get a ZIP back with every string translated and every variable locked in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What LocaFile AI Actually Does
&lt;/h2&gt;

&lt;p&gt;LocaFile AI is a no-login, pay-per-pass AI game localization web app. It's built for solo developers and small indie studios shipping on Steam — the people who need 8 languages for a Steam Next Fest demo but don't have a localization budget.&lt;/p&gt;

&lt;p&gt;The core flow is dead simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt; — drag and drop your game string file. The parser detects the format, counts words, and validates structure. No account required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview for free&lt;/strong&gt; — paste a few lines and run a free syntax check. You can even test a real AI translation pass on up to 5 lines before paying anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pay once&lt;/strong&gt; — a flat fee via Stripe. You get an entitlement token, not a user account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get your ZIP&lt;/strong&gt; — the translation job runs through a three-pass AI pipeline, bundles everything, and delivers it via on-screen download plus email.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire process takes minutes, not weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Variable Problem: Why Most Translation Tools Break Your Game
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody tells you about android string translation: it's not the words that break your app. It's the variables.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;strings.xml&lt;/code&gt; probably contains entries like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;string&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"welcome_message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Welcome back, %1$s! You have {unread_count} new messages.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"health_display"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;color&lt;/span&gt;&lt;span class="err"&gt;=red&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;HP:&lt;span class="nt"&gt;&amp;lt;/color&amp;gt;&lt;/span&gt; {current_hp}/{max_hp}&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A generic translation service sees &lt;code&gt;%1$s&lt;/code&gt;, &lt;code&gt;{unread_count}&lt;/code&gt;, and &lt;code&gt;&amp;lt;color=red&amp;gt;&lt;/code&gt; as text to translate. It "helpfully" rewrites them, reorders them, or drops them entirely. The result? Your app crashes on launch, or worse, displays &lt;code&gt;[[MISSING]]&lt;/code&gt; placeholders to players in 8 languages.&lt;/p&gt;

&lt;p&gt;This is the problem LocaFile AI was built to solve. The core differentiator is a &lt;strong&gt;VariableLocker&lt;/strong&gt; system with 10 variable-matcher presets: &lt;code&gt;double_curly&lt;/code&gt;, &lt;code&gt;dollar_curly&lt;/code&gt;, &lt;code&gt;single_curly&lt;/code&gt;, &lt;code&gt;square_bracket&lt;/code&gt;, &lt;code&gt;smart_string&lt;/code&gt;, &lt;code&gt;printf&lt;/code&gt;, &lt;code&gt;rich_text&lt;/code&gt;, &lt;code&gt;laravel_colon&lt;/code&gt;, &lt;code&gt;button_glyph&lt;/code&gt;, and &lt;code&gt;subtitle_tag&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before the AI ever sees your text, every variable is swapped for a &lt;code&gt;[[LOCK_N]]&lt;/code&gt; token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before: "Welcome back, %1$s! You have {unread_count} new messages."
After:   "Welcome back, [[LOCK_1]]! You have [[LOCK_2]] new messages."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI translates the surrounding text. Then the locker restores your variables byte-for-byte. A validator checks every single one. You get a per-language QA report proving nothing was mangled.&lt;/p&gt;

&lt;p&gt;This matters for every format, not just Android. The same engine handles Godot CSV/TSV/PO files, Unity CSV/YAML/JSON, Ren'Py scripts, Unreal projects, Flutter ARB files, and subtitle formats like SRT, VTT, ASS, and SBV. The android strings.xml translation online workflow is just one of 17 format parsers under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Android Strings XML Translation Workflow
&lt;/h2&gt;

&lt;p&gt;Let's walk through the actual android translate strings.xml process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Upload and Analyze
&lt;/h3&gt;

&lt;p&gt;Go to locafileai.com, drag your &lt;code&gt;strings.xml&lt;/code&gt; onto the upload zone. The parser detects the format, validates the XML structure, counts words, and gives you a price. No signup form. No "create an account to see pricing" wall.&lt;/p&gt;

&lt;p&gt;If you're not sure whether your file is valid, paste a snippet into the free syntax checker at &lt;code&gt;/analyze&lt;/code&gt;. It'll flag malformed XML, duplicate keys, or unescaped quotes before you spend a cent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Free Preview
&lt;/h3&gt;

&lt;p&gt;This is where LocaFile AI differs from every android string translation tool that makes you pay upfront and pray. You can lock variables on your pasted string with zero AI cost, then run one real AI pass on up to 5 lines. You'll see exactly how the variable locking works before you commit.&lt;/p&gt;

&lt;p&gt;The preview is throttled at 10 requests per minute because every AI call costs money — but for a 5-line test, you'll never hit that limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Pay and Translate
&lt;/h3&gt;

&lt;p&gt;Pick your tier. The &lt;strong&gt;Indie Starter&lt;/strong&gt; at $19 is an impulse buy for solo devs prepping a Steam Next Fest demo. The &lt;strong&gt;Steam Launchpad&lt;/strong&gt; at $99 fits small studios shipping a full launch. The &lt;strong&gt;Studio Pass&lt;/strong&gt; at $249 covers multi-SKU and DLC workflows.&lt;/p&gt;

&lt;p&gt;Pay through Stripe Checkout. The webhook creates your entitlement — a UUID, not a user account — and dispatches the translation job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: The Three-Pass Pipeline
&lt;/h3&gt;

&lt;p&gt;Your file doesn't get one pass through an AI model. It gets three:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pass one&lt;/strong&gt; locks all variables and does the initial translation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass two&lt;/strong&gt; reviews for consistency, context, and tone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass three&lt;/strong&gt; validates every variable was restored correctly and runs quality checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each language gets a &lt;code&gt;TranslationQaReport&lt;/code&gt;. If a variable was dropped, the pipeline catches it before you ever download the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Download and Ship
&lt;/h3&gt;

&lt;p&gt;You get a ZIP with all your translated &lt;code&gt;strings.xml&lt;/code&gt; files, organized by locale. The download link expires after 24 hours, but you'll also get the files via email. Lose the link? The &lt;code&gt;/recover&lt;/code&gt; endpoint regenerates it from your UUID.&lt;/p&gt;

&lt;p&gt;If a job fails partway through, you can retry without re-paying — the pipeline resumes from the completed languages instead of starting over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Beats the Alternatives
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Agencies&lt;/strong&gt; deliver quality but cost $2,000+ and take weeks. You don't have weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raw ChatGPT&lt;/strong&gt; is free but will mangle your &lt;code&gt;{player_name}&lt;/code&gt; tags, skip long strings, and produce inconsistent terminology across 8 languages. You'll spend more time fixing broken XML than you saved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other online tools&lt;/strong&gt; treat translation as a text swap. They don't understand game string formats, variable placeholders, or the difference between &lt;code&gt;%1$s&lt;/code&gt; and &lt;code&gt;%s&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;LocaFile AI is the android strings xml translation tool that treats your variables as inviolable. The entire architecture — the VariableLocker, the three-pass pipeline, the QA reports — exists to guarantee one thing: &lt;strong&gt;your app won't crash when translated.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you're a solo dev or small studio shipping on Steam, you need android strings XML translation online that doesn't require an account, doesn't charge per word, and won't break your game. &lt;a href="https://locafileai.com" rel="noopener noreferrer"&gt;LocaFile AI&lt;/a&gt; delivers exactly that: upload, pay a flat fee, download your ZIP, ship your game.&lt;/p&gt;

&lt;p&gt;No login. No subscription. No fixing broken tags at 2 AM.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Generate OG Images at Scale: A Developer's Guide</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:55:45 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/how-to-generate-og-images-at-scale-a-developers-guide-56pl</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/how-to-generate-og-images-at-scale-a-developers-guide-56pl</guid>
      <description>&lt;h1&gt;
  
  
  How to Generate OG Images at Scale: A Developer's Guide
&lt;/h1&gt;

&lt;p&gt;The link preview is the first thing people see before they click. On X, LinkedIn, Slack, and iMessage, your URL renders as a 1200×630 card — and if that card is a generic gray box with your domain name, you've already lost the click to the person who posted a screenshot instead.&lt;/p&gt;

&lt;p&gt;Dynamic Open Graph images fix this. Instead of one static PNG per page, you render a unique card per URL — title, author, reading time, a chart, whatever your page actually contains. The result is a measurable lift in click-through rate and share velocity. But &lt;em&gt;how&lt;/em&gt; you generate those images matters more than whether you do it at all. Get the architecture wrong and you'll ship broken cards, burn server resources, or lock yourself into a tool that can't scale.&lt;/p&gt;

&lt;p&gt;Here's the landscape, the trade-offs, and a pipeline that holds up under real traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Landscape: Client-Side vs. Server-Side Rendering
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Client-side generation&lt;/strong&gt; is the trap. Tools like myogimage.com let you design a template in the browser and export a static PNG. That's fine for a one-off — but it collapses the moment you need dynamic content. You can't render a card for a blog post that doesn't exist yet, and you can't automate a pipeline around a GUI. Worse, some of these "free" tools advertise an API that doesn't actually exist. If your OG strategy depends on a service that can't be called programmatically, you don't have a strategy — you have a screenshot.&lt;/p&gt;

&lt;p&gt;The other client-side trap is generating images in the browser at request time. Social scrapers (Twitterbot, Slackbot, LinkedInBot) don't execute JavaScript. If your OG image is rendered client-side, the scraper sees nothing. This is the single most common reason "my OG image works in the preview but not when I share it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-side rendering&lt;/strong&gt; is the only approach that works reliably. The scraper makes a GET request, your server returns a complete PNG. No JavaScript required. Within server-side, you have two real options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DIY with &lt;code&gt;satori&lt;/code&gt; + a deployment platform&lt;/strong&gt; (the Vercel approach). &lt;code&gt;satori&lt;/code&gt; converts JSX to SVG, then you rasterize to PNG with &lt;code&gt;resvg&lt;/code&gt; or &lt;code&gt;sharp&lt;/code&gt;. It's fast, it's free, and it's fully under your control. The cost is yours too: you maintain the rendering service, handle font loading, manage concurrency, and debug edge cases yourself. It's a solid choice if you have the time and the traffic justifies the infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A dedicated OG image API&lt;/strong&gt; (like FastOG). You send a GET request with your template and parameters, the service renders the image server-side, caches it at the edge, and returns a URL. No infrastructure to maintain, no fonts to bundle, no scraper compatibility to test. You pay per image, and caching means repeat renders cost nothing.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The right choice depends on your constraints. If you're an indie hacker shipping a blog this weekend, the DIY stack is a fun afternoon project. If you're building a SaaS where every page needs a unique card and your time is better spent on the product, a managed API wins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Anatomy of a High-Performance OG Image Pipeline
&lt;/h2&gt;

&lt;p&gt;A production-grade pipeline has five stages. Here's what each one needs to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. URL Design
&lt;/h3&gt;

&lt;p&gt;Your OG image endpoint should be a simple GET request with query parameters — no auth headers, no POST bodies. Social scrapers only send GET requests. FastOG's pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.fastog.com/api/v1/og?template=blog&amp;amp;title=Hello%20World&amp;amp;author=Jane
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the parameter surface small. Every parameter is a template variable you need to document, validate, and test. Start with 3–5 per template.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Template Registry
&lt;/h3&gt;

&lt;p&gt;A template is a design with named slots: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;reading_time&lt;/code&gt;, &lt;code&gt;accent_color&lt;/code&gt;. Your registry maps a template ID to its layout and default styles. FastOG ships 41 templates covering OG cards (1200×630) and X headers (1500×500) — but you don't need 41. You need a handful that match your brand and a registry that makes adding new ones trivial.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rendering
&lt;/h3&gt;

&lt;p&gt;The renderer takes the template, injects the parameters, and produces a PNG. This is where &lt;code&gt;satori&lt;/code&gt;-style approaches and managed APIs diverge. The key performance metric is &lt;strong&gt;time-to-first-byte&lt;/strong&gt;: social scrapers are impatient. If your render takes longer than ~2 seconds, platforms will fall back to a generic card. FastOG renders via Svelte → headless Chrome, which handles complex layouts and custom fonts gracefully, and serves the result from edge cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Caching
&lt;/h3&gt;

&lt;p&gt;This is the difference between "paying per image" and "paying per unique image." If your pipeline caches at the CDN edge, the first render of a URL is the only render that costs anything. Every subsequent scrape — and there will be many, every time someone shares the link — hits the cache. FastOG's model is 1 credit = 1 image, with 0 cost on cache hits. If you're DIY, make sure your cache layer is in front of your renderer, not behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Security
&lt;/h3&gt;

&lt;p&gt;If your OG endpoint accepts query parameters, it's a public URL. Anyone can hit it. That's fine for public content, but it means you need &lt;strong&gt;HMAC signing&lt;/strong&gt; for anything you don't want rendered arbitrarily. The pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js example: sign an OG image URL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&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;secret&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&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;sig=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.fastog.com/api/v1/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;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&lt;/span&gt;&lt;span class="dl"&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="s1"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OG_SECRET&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server recomputes the HMAC and rejects requests with an invalid or missing signature. This prevents abuse of your render quota and stops people from generating arbitrary images on your dime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;Here's a minimal implementation for a blog, using a managed API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/og.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OG_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.fastog.com/api/v1/og&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;OG_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OG_SECRET&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;function&lt;/span&gt; &lt;span class="nf"&gt;getOgImageUrl&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;excerpt&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;excerpt&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="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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OG_SECRET&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&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="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OG_BASE&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;sig=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sig&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;p&gt;Then in your page template, add the meta tags:&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;{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:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;{getOgImageUrl({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;excerpt&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt; &lt;span class="err"&gt;})}&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's it. The scraper fetches the image URL, your API renders and caches it, and every share of that page shows a custom card.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Client-side OG tools are fine for a static site with five pages. The moment you need dynamic cards — per-post, per-user, per-product — you need a server-side pipeline. Whether you build it with &lt;code&gt;satori&lt;/code&gt; or buy it from a managed API depends on your time budget and traffic. But the architecture is non-negotiable: GET-based URL, template registry, fast renderer, edge cache, and HMAC signing.&lt;/p&gt;

&lt;p&gt;Start with one template and one page type. Measure your click-through rate before and after. When you see the lift, expand to the rest of your site. The first card is the hardest — everything after that is just filling in the template.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Generate OG Images at Scale: A Developer's Guide</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:52:36 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/how-to-generate-og-images-at-scale-a-developers-guide-k4h</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/how-to-generate-og-images-at-scale-a-developers-guide-k4h</guid>
      <description>&lt;h1&gt;
  
  
  How to Generate OG Images at Scale: A Developer's Guide
&lt;/h1&gt;

&lt;p&gt;The link preview is the first thing people see before they click. On X, LinkedIn, Slack, and iMessage, your URL renders as a 1200×630 card — and if that card is a generic gray box with your domain name, you've already lost the click to the person who posted a screenshot instead.&lt;/p&gt;

&lt;p&gt;Dynamic Open Graph images fix this. Instead of one static PNG per page, you render a unique card per URL — title, author, reading time, a chart, whatever your page actually contains. The result is a measurable lift in click-through rate and share velocity. But &lt;em&gt;how&lt;/em&gt; you generate those images matters more than whether you do it at all. Get the architecture wrong and you'll ship broken cards, burn server resources, or lock yourself into a tool that can't scale.&lt;/p&gt;

&lt;p&gt;Here's the landscape, the trade-offs, and a pipeline that holds up under real traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Landscape: Client-Side vs. Server-Side Rendering
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Client-side generation&lt;/strong&gt; is the trap. Tools like myogimage.com let you design a template in the browser and export a static PNG. That's fine for a one-off — but it collapses the moment you need dynamic content. You can't render a card for a blog post that doesn't exist yet, and you can't automate a pipeline around a GUI. Worse, some of these "free" tools advertise an API that doesn't actually exist. If your OG strategy depends on a service that can't be called programmatically, you don't have a strategy — you have a screenshot.&lt;/p&gt;

&lt;p&gt;The other client-side trap is generating images in the browser at request time. Social scrapers (Twitterbot, Slackbot, LinkedInBot) don't execute JavaScript. If your OG image is rendered client-side, the scraper sees nothing. This is the single most common reason "my OG image works in the preview but not when I share it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-side rendering&lt;/strong&gt; is the only approach that works reliably. The scraper makes a GET request, your server returns a complete PNG. No JavaScript required. Within server-side, you have two real options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DIY with &lt;code&gt;satori&lt;/code&gt; + a deployment platform&lt;/strong&gt; (the Vercel approach). &lt;code&gt;satori&lt;/code&gt; converts JSX to SVG, then you rasterize to PNG with &lt;code&gt;resvg&lt;/code&gt; or &lt;code&gt;sharp&lt;/code&gt;. It's fast, it's free, and it's fully under your control. The cost is yours too: you maintain the rendering service, handle font loading, manage concurrency, and debug edge cases yourself. It's a solid choice if you have the time and the traffic justifies the infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A dedicated OG image API&lt;/strong&gt; (like FastOG). You send a GET request with your template and parameters, the service renders the image server-side, caches it at the edge, and returns a URL. No infrastructure to maintain, no fonts to bundle, no scraper compatibility to test. You pay per image, and caching means repeat renders cost nothing.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The right choice depends on your constraints. If you're an indie hacker shipping a blog this weekend, the DIY stack is a fun afternoon project. If you're building a SaaS where every page needs a unique card and your time is better spent on the product, a managed API wins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Anatomy of a High-Performance OG Image Pipeline
&lt;/h2&gt;

&lt;p&gt;A production-grade pipeline has five stages. Here's what each one needs to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. URL Design
&lt;/h3&gt;

&lt;p&gt;Your OG image endpoint should be a simple GET request with query parameters — no auth headers, no POST bodies. Social scrapers only send GET requests. FastOG's pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.fastog.com/api/v1/og?template=blog&amp;amp;title=Hello%20World&amp;amp;author=Jane
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the parameter surface small. Every parameter is a template variable you need to document, validate, and test. Start with 3–5 per template.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Template Registry
&lt;/h3&gt;

&lt;p&gt;A template is a design with named slots: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;reading_time&lt;/code&gt;, &lt;code&gt;accent_color&lt;/code&gt;. Your registry maps a template ID to its layout and default styles. FastOG ships 41 templates covering OG cards (1200×630) and X headers (1500×500) — but you don't need 41. You need a handful that match your brand and a registry that makes adding new ones trivial.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rendering
&lt;/h3&gt;

&lt;p&gt;The renderer takes the template, injects the parameters, and produces a PNG. This is where &lt;code&gt;satori&lt;/code&gt;-style approaches and managed APIs diverge. The key performance metric is &lt;strong&gt;time-to-first-byte&lt;/strong&gt;: social scrapers are impatient. If your render takes longer than ~2 seconds, platforms will fall back to a generic card. FastOG renders via Svelte → headless Chrome, which handles complex layouts and custom fonts gracefully, and serves the result from edge cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Caching
&lt;/h3&gt;

&lt;p&gt;This is the difference between "paying per image" and "paying per unique image." If your pipeline caches at the CDN edge, the first render of a URL is the only render that costs anything. Every subsequent scrape — and there will be many, every time someone shares the link — hits the cache. FastOG's model is 1 credit = 1 image, with 0 cost on cache hits. If you're DIY, make sure your cache layer is in front of your renderer, not behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Security
&lt;/h3&gt;

&lt;p&gt;If your OG endpoint accepts query parameters, it's a public URL. Anyone can hit it. That's fine for public content, but it means you need &lt;strong&gt;HMAC signing&lt;/strong&gt; for anything you don't want rendered arbitrarily. The pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js example: sign an OG image URL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&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;secret&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&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;sig=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.fastog.com/api/v1/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;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&lt;/span&gt;&lt;span class="dl"&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="s1"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OG_SECRET&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server recomputes the HMAC and rejects requests with an invalid or missing signature. This prevents abuse of your render quota and stops people from generating arbitrary images on your dime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;Here's a minimal implementation for a blog, using a managed API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/og.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OG_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.fastog.com/api/v1/og&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;OG_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OG_SECRET&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;function&lt;/span&gt; &lt;span class="nf"&gt;getOgImageUrl&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;excerpt&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;excerpt&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="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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OG_SECRET&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&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="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OG_BASE&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;sig=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sig&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;p&gt;Then in your page template, add the meta tags:&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;{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:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;{getOgImageUrl({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;excerpt&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt; &lt;span class="err"&gt;})}&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's it. The scraper fetches the image URL, your API renders and caches it, and every share of that page shows a custom card.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Client-side OG tools are fine for a static site with five pages. The moment you need dynamic cards — per-post, per-user, per-product — you need a server-side pipeline. Whether you build it with &lt;code&gt;satori&lt;/code&gt; or buy it from a managed API depends on your time budget and traffic. But the architecture is non-negotiable: GET-based URL, template registry, fast renderer, edge cache, and HMAC signing.&lt;/p&gt;

&lt;p&gt;Start with one template and one page type. Measure your click-through rate before and after. When you see the lift, expand to the rest of your site. The first card is the hardest — everything after that is just filling in the template.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Translate Game Localization Files (.JSON / .PO) Without Breaking Code Variables</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Mon, 27 Jul 2026 18:38:44 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/how-to-translate-game-localization-files-json-po-without-breaking-code-variables-2334</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/how-to-translate-game-localization-files-json-po-without-breaking-code-variables-2334</guid>
      <description>&lt;p&gt;Introduction: The Scale vs. Code Integrity Dilemma&lt;br&gt;
Localizing an indie game or a software product is no longer optional. On platforms like Steam, offering the standard 28-language catalogue (including Simplified Chinese, Japanese, Korean, German, and Brazilian Portuguese) routinely unlocks over 50% of a game's total revenue potential.&lt;br&gt;
Yet, indie game developers face a brutal trade-off:&lt;br&gt;
Human Translation Agencies: Charging $\$0.10$ to $\$0.20$ per word. Localizing a modest 20,000-word game into 28 languages can easily cost $\$30,000+$, completely pricing out solo developers and small teams.&lt;br&gt;
Traditional Enterprise SaaS (Lokalise, Crowdin): Charging $\$50$ to $\$200+$ per month just for workspace access, forcing developers into bloated workflows and complex account setups.&lt;br&gt;
Naïve Single-Pass AI (Raw ChatGPT/Claude): Fast and cheap, but destructive to game builds.&lt;/p&gt;

&lt;p&gt;This article explores the technical breakdown of why standard LLMs fail when processing code files, and details the engineering architecture required to build a code-safe, zero-friction localization pipeline.&lt;br&gt;
Section 1: Anatomy of a Build Crash - Why Raw LLMs Corrupt&amp;nbsp;Code&lt;br&gt;
Large Language Models (LLMs) are statistical text predictors designed for natural human prose. They lack an internal compiler or Abstract Syntax Tree (AST) context when reading a raw file.&lt;br&gt;
When you paste a&amp;nbsp;.json,&amp;nbsp;.po,&amp;nbsp;.yaml, or&amp;nbsp;.csv file into a standard LLM, five distinct failure modes occur:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Variable &amp;amp; Token&amp;nbsp;Mutation
Game engines rely on runtime string interpolation. When an LLM encounters:
JSON
"hud_gold": "You collected {gold_amount} coins from {monster_type}."
It frequently translates the variable names inside the brackets:
JSON
"hud_gold": "Você coletou {quantidade_ouro} moedas de {tipo_monstro}." // ❌ CRASH: Engine looks for {gold_amount}&lt;/li&gt;
&lt;li&gt;Positional &amp;amp; Printf Specifier Mangling
C-style formatting specifiers like %s, %d, %f, or&amp;nbsp;:user are often dropped, reordered without positional indexes, or spaced out:
JSON
"save_slot": "Save file %s loaded." -&amp;gt; "Save file % s loaded." // ❌ PARSE ERROR&lt;/li&gt;
&lt;li&gt;ICU Plural Rule Destruction
Complex game strings use ICU plural syntax to handle numerical agreement:
Plaintext
{count, plural, =0 {No items} =1 {One item} other {# items}}
Standard LLMs frequently translate the structural control keywords (plural, other), breaking the engine's localization parser entirely.&lt;/li&gt;
&lt;li&gt;Rich Text &amp;amp; HTML Tag Pollution
Game engines (like Unity's TextMeshPro or Godot's RichTextLabel) use inline markup like  or [b]. LLMs often translate color names (), drop closing tags, or convert angle brackets into unescaped HTML entities (&amp;lt;color&amp;gt;).&lt;/li&gt;
&lt;li&gt;Unescaped Quotes and Structural Syntax&amp;nbsp;Errors
In JSON or YAML files, if a translated French or Spanish string contains an unescaped double quote (e.g., "desc": "L'épée de l' "héros" "), the resulting output becomes invalid JSON, breaking the entire game build at boot.
Section 2: Architecture of an Engine-Aware Localization Engine
To guarantee zero build breaks, the translation process must be decoupled from the raw file structure. The LLM must never be allowed to touch raw code variables.
Here is the technical architecture behind LocaFile AI:
Plaintext
┌────────────────┐     ┌───────────────────────┐     ┌───────────────────────┐
│ Source File    │ ──&amp;gt; │ Engine Regex Parser   │ ──&amp;gt; │ Token Masking         │
│ (JSON/PO/CSV)  │     │ Auto-Detect Format    │     │ {var} ──&amp;gt; [[LOCK_0]]  │
└────────────────┘     └───────────────────────┘     └───────────────────────┘
                                                             │
┌────────────────┐     ┌───────────────────────┐                 ▼
│ Delivered ZIP  │ &amp;lt;── │ Post-Processing QA    │ &amp;lt;── ┌───────────────────────┐
│ (28 Languages) │     │ AST Validation &amp;amp; Fix  │     │ 3-Pass AI Engine      │
└────────────────┘     └───────────────────────┘     │ + Lore &amp;amp; Tone Rules   │
                                                 └───────────────────────┘
A. Engine Format Auto-Detection &amp;amp; AST&amp;nbsp;Parsing
The system supports 9 engine formats out of the box:
Godot:&amp;nbsp;.po, Godot&amp;nbsp;.csv (multi-column
Unity: Unity&amp;nbsp;.csv, i18n&amp;nbsp;.json
Unreal &amp;amp; Mobile: XLIFF, iOS&amp;nbsp;.strings, Android&amp;nbsp;.xml
Web Frameworks:&amp;nbsp;.yaml,&amp;nbsp;.tsv, Laravel PHP lang arrays&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An AST parser extracts translatable values while maintaining the exact key mapping, structural nested hierarchy, and array index positions.&lt;br&gt;
B. The Immutable Variable&amp;nbsp;Locker&lt;br&gt;
Before any prompt is constructed, a multi-stage Regex scanner identifies all control variables, HTML/rich-text tags, positional specifiers, and ICU blocks:&lt;br&gt;
Detection: Identifies patterns matching {...}, %s, %d,&amp;nbsp;:key, , [tag], and indexed placeholders {0}.&lt;br&gt;
Replacement: Replaces each detected variable with an immutable, deterministic lock token: [[LOCK_0]], [[LOCK_1]], etc.&lt;br&gt;
Why Double Square Brackets? Double square bracket tokens ([[LOCK_N]]) do not naturally occur in legitimate game dialogue or software strings. They prevent LLM tokenization distortion and survive the AI translation pass untouched.&lt;/p&gt;

&lt;p&gt;C. Lore Glossaries &amp;amp; Prompt Injection Protection&lt;br&gt;
Game lore uses proper nouns (character names, fictional planets, unique spells) that must remain identical across all languages.&lt;br&gt;
Lore Glossary Engine: Developers can pass up to 2,000 characters of proper nouns (e.g., "CyberCore, Eldoria, X-400").&lt;br&gt;
Security: Glossaries are validated against strict prompt-injection detection patterns before being appended as immutable system constraints.&lt;/p&gt;

&lt;p&gt;D. Context &amp;amp; Tone Customization&lt;br&gt;
Language register heavily impacts player immersion. A medieval RPG demands formal or archaic phrasing, while a modern shooter requires punchy, casual dialogue.&lt;br&gt;
Developers can specify target registers:&lt;br&gt;
Standard: Clean, neutral UI phrasing.&lt;br&gt;
Casual: Natural, conversational registers (e.g., informal "tu" in French/Spanish).&lt;br&gt;
Fantasy / Heroic: Immersive register for RPGs and story-driven titles.&lt;br&gt;
Sci-Fi / Cyberpunk: Stylized technical phrasing.&lt;/p&gt;

&lt;p&gt;Section 3: The Automated Post-Processing QA &amp;amp; Auto-Fix&amp;nbsp;Pipeline&lt;br&gt;
Even with prompt constraints, an production-grade system cannot rely solely on the LLM's output. A dedicated post-processing engine must validate every single key before packaging.&lt;/p&gt;


&lt;ol&gt;

&lt;li&gt;&lt;p&gt;Linguistic Variable Checker (Regex AST Validator)&lt;br&gt;&lt;br&gt;
Before delivering the final ZIP bundle, a dedicated post-processing engine compares the source string against the target translation across 5 variable patterns:&lt;br&gt;&lt;br&gt;
Curly Brackets: {player_name}&lt;br&gt;&lt;br&gt;
Colon Specifiers:&amp;nbsp;:attribute&lt;br&gt;&lt;br&gt;
Printf Tokens: %s, %d&lt;br&gt;&lt;br&gt;
Markup Tags: , &lt;b&gt;&lt;br&gt;&lt;br&gt;
Indexed Tokens: {0}, {1}&lt;/b&gt;&lt;/p&gt;&lt;/li&gt;
&lt;b&gt;&lt;br&gt;
&lt;li&gt;&lt;p&gt;Defect Detection &amp;amp; Auto-Fix&amp;nbsp;Engine&lt;br&gt;&lt;br&gt;
The QA validator flags three types of issues:&lt;br&gt;&lt;br&gt;
Missing Variables: The source string had {count}, but the translation dropped it.&lt;br&gt;&lt;br&gt;
Corrupted Variables: The AI altered the internal token (e.g., [[LOCK_0]] became [[LOCK_0_ES]]).&lt;br&gt;&lt;br&gt;
Extra Variables: Unsanitized tokens present in translation but absent in source.&lt;/p&gt;&lt;/li&gt;
&lt;br&gt;
&lt;/b&gt;
&lt;/ol&gt;
&lt;b&gt;

&lt;/b&gt;&lt;p&gt;Automated Re-Injection: When a missing variable is detected, the engine calculates its semantic position relative to surrounding words and automatically re-injects the missing {variable} into the translated string before generating a detailed report (quality-report.json).&lt;br&gt;
Section 4: Pay-Per-Pass vs. Monthly Subscriptions&lt;br&gt;
The SaaS industry is saturated with recurring monthly subscriptions that penalize developers during idle development phases. Indie game development is inherently bursty: you might localize heavily during a major launch or playtest, and then go months without needing translation updates.&lt;br&gt;
Why Pay-Per-Pass Wins for Indie Game&amp;nbsp;Devs:&lt;br&gt;
PRICING COMPARISON:&lt;br&gt;
Human Agencies&amp;nbsp;: $0.10 - $0.20/word | High Cost | Manual Variable Handling&lt;br&gt;
Enterprise SaaS&amp;nbsp;: $50 - $200+/month | Subscription| Complex Workspace Setup&lt;br&gt;
LocaFile AI&amp;nbsp;: From $19 (Flat) | Pay-per-Pass| Automated Variable Protection&lt;br&gt;
Indie Starter ($19): 3 runs, up to 5,000 words/file across 6 core languages (EFIGS + CJK).&lt;br&gt;
Steam Launchpad ($99): 10 runs, up to 25,000 words/file across all 28 Steam catalogue languages.&lt;br&gt;
Studio Pass ($249): 35 runs, up to 100,000 words/file for large-scale RPGs and studio pipelines.&lt;/p&gt;

&lt;p&gt;Section 5: Hands-On Tutorial - Localizing a Game in 3&amp;nbsp;Steps&lt;br&gt;
Step 1: Prepare Your Game&amp;nbsp;File&lt;br&gt;
Ensure your string keys are separated from translatable values in a supported format:&lt;br&gt;
JSON&lt;br&gt;
{&lt;br&gt;
  "quest_header": "Quest: Defeat {boss_name}",&lt;br&gt;
  "item_reward": "You earned {amount} gold coins!",&lt;br&gt;
  "dialogue_intro": "Welcome to Eldoria, traveler."&lt;br&gt;
}&lt;br&gt;
Step 2: Test Variable Protection (Zero-Cost Live&amp;nbsp;Preview)&lt;br&gt;
Navigate to locafileai.com and use the zero-cost Variable Protection Preview tool on the homepage.&lt;br&gt;
Paste your raw JSON snippet to observe the production VariableLocker instantly convert {boss_name} and  into protected [[LOCK_N]] tokens in real-time before initiating any pass.&lt;br&gt;
JSON&lt;br&gt;
// Real-time Variable Masking Output:&lt;br&gt;
{&lt;br&gt;
  "quest_header": "Quest: Defeat [[LOCK_0]]",&lt;br&gt;
  "item_reward": "You earned [[LOCK_1]][[LOCK_2]][[LOCK_3]] gold coins!",&lt;br&gt;
  "dialogue_intro": "Welcome to Eldoria, traveler."&lt;br&gt;
}&lt;br&gt;
Step 3: Run the Pass &amp;amp; Import to&amp;nbsp;Engine&lt;br&gt;
Drop File or ZIP Batch: Drag your&amp;nbsp;.json,&amp;nbsp;.po, or&amp;nbsp;.csv files directly onto the dropzone.&lt;br&gt;
Set Glossaries &amp;amp; Tone: Add proper nouns to your Lore Glossary and select your game's tone (e.g., Fantasy or Casual).&lt;br&gt;
Execute Pass: Select your target languages (or full 28 Steam catalogue) and watch live real-time WebSocket progress via Laravel Reverb.&lt;br&gt;
Export &amp;amp; Build: Download your validated ZIP bundle and drop it directly into your Godot res:// or Unity Assets/Resources/ directories. Zero syntax errors, zero build breaks.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;br&gt;
Game localization should not require thousands of dollars in agency fees or hours spent debugging broken JSON brackets and mangled variable names. By isolating variables through pre-parsing AST Regex lockers, applying post-processing QA validation, and offering a fair pay-per-pass pricing model, developers can expand their games to a global Steam audience with total code safety.&lt;br&gt;
Test the real-time variable protection preview today at locafileai.com!&lt;/p&gt;



</description>
      <category>gamedev</category>
      <category>softwaredevelopment</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Localize a Godot Game Without Breaking Variable Tags</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Mon, 27 Jul 2026 18:36:12 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/how-to-localize-a-godot-game-without-breaking-variable-tags-3gm8</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/how-to-localize-a-godot-game-without-breaking-variable-tags-3gm8</guid>
      <description>&lt;h1&gt;
  
  
  How to Localize a Godot Game Without Breaking Variable Tags
&lt;/h1&gt;

&lt;p&gt;Godot's localization system is built around a simple idea: separate your text from your code, translate the text, and let the engine swap it at runtime. In practice, the variable tags inside those strings are where things fall apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Godot handles translatable strings
&lt;/h2&gt;

&lt;p&gt;Godot, a &lt;a href="https://godotengine.org/features/" rel="noopener noreferrer"&gt;free, open-source game engine&lt;/a&gt; used by solo developers and small studios for 2D and 3D projects, stores translatable text in CSV files or .po (gettext) files. You define a key, write your source string, and add columns or entries for each target language.&lt;/p&gt;

&lt;p&gt;A typical CSV row might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;QUEST&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;COMPLETE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"{player_name} finished the quest in {time} seconds"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in GDScript, you call it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"{player_name} finished the quest in {time} seconds"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="n"&gt;player_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;weapon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsed&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;Godot also supports C-style format strings (&lt;code&gt;%s&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt;) and rich text tags like &lt;code&gt;[color=red]&lt;/code&gt; in its RichTextLabel nodes. These are all over a typical game's translation file, and they all need to survive translation intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where translations break
&lt;/h2&gt;

&lt;p&gt;The problem is not the translation itself. It is the variable tags inside the strings.&lt;/p&gt;

&lt;p&gt;When you paste &lt;code&gt;{player_name} picked up the {weapon}&lt;/code&gt; into a translation tool or general-purpose AI, several things can go wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Curly braces get translated.&lt;/strong&gt; Some tools interpret &lt;code&gt;{player_name}&lt;/code&gt; as natural language and translate "player" and "name" into the target language. Your code calls &lt;code&gt;.format()&lt;/code&gt; with the English key. It finds no match. The placeholder renders as raw text, or your game crashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format specifiers get reordered.&lt;/strong&gt; &lt;code&gt;%s killed %d enemies&lt;/code&gt; depends on argument order. A translator might flip the sentence structure for grammar but leave &lt;code&gt;%s&lt;/code&gt; and &lt;code&gt;%d&lt;/code&gt; in the original positions, swapping which value fills which slot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich text tags get mangled.&lt;/strong&gt; &lt;code&gt;[color=red]Warning[/color]&lt;/code&gt; might become &lt;code&gt;[couleur=rouge]Avertissement[/couleur]&lt;/code&gt; in French output, breaking BBCode parsing entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whitespace around tags disappears.&lt;/strong&gt; Tokenizers strip or merge spaces near braces and brackets, producing &lt;code&gt;{player_name}picked up&lt;/code&gt; with no space between the variable and the next word.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These bugs are silent. The CSV file looks fine. The game launches. Then a Japanese player sees &lt;code&gt;{player_name}&lt;/code&gt; rendered as literal text on screen because the translator converted the curly braces to full-width characters.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to protect your tags
&lt;/h2&gt;

&lt;p&gt;The reliable approach is to replace every variable tag with a placeholder token before translation, then restore the originals after.&lt;/p&gt;

&lt;p&gt;Here is the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scan each string for variable patterns.&lt;/strong&gt; Match &lt;code&gt;{...}&lt;/code&gt;, &lt;code&gt;%s&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt;, &lt;code&gt;%f&lt;/code&gt;, &lt;code&gt;[color=...]...[/color]&lt;/code&gt;, and any custom tags your project uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace each match with a numbered token&lt;/strong&gt; like &lt;code&gt;__VAR_0__&lt;/code&gt;, &lt;code&gt;__VAR_1__&lt;/code&gt;. Store the mapping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send only the tokenized string to translation.&lt;/strong&gt; The translator (human or AI) sees plain text with inert tokens it has no reason to modify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After translation, swap the tokens back&lt;/strong&gt; to the original variable tags, byte for byte.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the approach &lt;a href="//www.locafileai.com"&gt;LocaFile AI&lt;/a&gt; uses. When you upload a Godot CSV or .po file, it detects and locks every variable pattern, including &lt;code&gt;{player_name}&lt;/code&gt;, &lt;code&gt;%s&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt;, and &lt;code&gt;&amp;lt;color=red&amp;gt;&lt;/code&gt; tags, with regex-based token replacement before the AI model sees the string. After translation, the original tags are restored exactly. The AI only ever processes plain text with inert placeholders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checks before you ship
&lt;/h2&gt;

&lt;p&gt;Even with tags protected, verify your translations before release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test with pseudolocalization.&lt;/strong&gt; Godot has a built-in pseudolocalization mode (Project Settings &amp;gt; Internationalization &amp;gt; Locale) that replaces your strings with accented, elongated versions. It catches hardcoded strings you forgot to externalize and UI elements that overflow with longer text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check font coverage.&lt;/strong&gt; Godot's default font only covers Latin-1 characters. For CJK, Cyrillic, Arabic, or Thai, load a font like Noto Sans as a DynamicFont resource and set it as your theme's default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle pluralization properly.&lt;/strong&gt; English has two plural forms. Polish has four. Arabic has six. Godot's &lt;code&gt;tr_n()&lt;/code&gt; function handles this per-locale, but your source strings need to be structured for it: &lt;code&gt;tr_n("There is %d apple", "There are %d apples", count)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test RTL languages.&lt;/strong&gt; If you are shipping in Arabic or Hebrew, Godot automatically mirrors UI anchors, margins, and control order. But icons with directional arrows (back/forward buttons) need manual attention.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The real cost of skipping localization
&lt;/h2&gt;

&lt;p&gt;Steam's audience is global. Roughly 60% of Steam users have their client set to a non-English language. Shipping English-only means your store page, your reviews, and your in-game text are invisible to most of the platform.&lt;/p&gt;

&lt;p&gt;The traditional path, hiring an agency, costs thousands of dollars per language and takes weeks. The shortcut of running your CSV through ChatGPT produces translations that look correct until &lt;code&gt;{player_name}&lt;/code&gt; shows up as literal text on a player's screen.&lt;/p&gt;

&lt;p&gt;Variable-safe translation closes that gap. Protect the tags, translate the text, verify with pseudolocalization, ship.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>programming</category>
      <category>software</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
