Dynamic Open Graph Images for Localized Pages

Within Metadata Injection & SEO Automation, this guide covers the images that appear when a page is shared on social networks and in messaging apps. For localized sites, the default approach, one generic image for every page or an English card reused across languages, wastes the most visible piece of a link preview. The guide shows how to generate Open Graph images from templates and CMS fields per locale, cache them efficiently and reference them correctly.

A link preview is often the first impression of a page. A card that shows the page’s title in the reader’s language, with the brand’s design, gets noticeably more clicks than a generic logo. Designing those cards by hand does not scale beyond a handful of pages, and certainly not across ten locales. Generating them from a template, with the title, category and image from the CMS, gives every page a proper card at no editorial cost.

Generating a localized card on first requestA social crawler requests the page and reads og:image, which points to an image route with locale, slug and version; on a cache miss the route fetches localized fields from the CMS, renders the template with the right font and returns a PNG that the CDN caches until the content version changes.Crawler readsog:image/og/de/slug?v=12CDNcached?Serve PNGRender templatelocalized fieldsyesnocache
Cards are generated lazily and cached per locale and content version.

The Problem

A travel company shared destination guides in nine languages on social media. All shares used the same Open Graph image, the destination’s hero photo without text, and the English title as og:title on every locale because the head template read the default-locale field. Social media managers in each market created localized cards by hand for campaigns, but organic shares by readers, the majority, showed English titles on French and Japanese timelines.

How Localized Card Generation Works

A template per card type. Designers create one or two templates, for example an article card and a product card, with areas for title, category, image and brand elements. Templates are built as JSX or SVG that a renderer turns into PNG.

Content from localized fields. The card route receives locale and slug, fetches the page’s localized title and other fields from the CMS, and renders the template in that locale, including right-to-left layout where needed.

Fonts per script. The renderer needs font files that contain the characters of each locale, such as a CJK font for Japanese pages, loaded only for those locales.

Versioned URLs. The og:image URL includes a content version, such as the entry’s revision number. When the title changes, the URL changes, the old cached card is no longer referenced, and platforms fetching the page get the new card.

A share on a messaging appA reader shares the French page; the app's crawler fetches the HTML, reads the localized og:title and og:image with a version, fetches the image from the CDN, which generates it on the first request, and shows a French preview card.ReaderMessaging appSite HTMLOG image routeshare /fr/guides/lisbonneGET pageog:title (fr), og:image ?v=12GET /og/fr/lisbonne?v=121200x630 PNGFrench preview card
Every step uses server-rendered data, because link preview crawlers do not run JavaScript.

Implementation

The image route renders the template with next/og in this example; Satori-based renderers work the same way in other frameworks.

TSX
// app/og/[locale]/[slug]/route.tsx
import { ImageResponse } from "next/og";
import { getGuide } from "@/lib/cms/guides";

const FONTS: Record<string, string> = { ja: "NotoSansJP-Bold.ttf", ar: "NotoSansArabic-Bold.ttf" };
const RTL = new Set(["ar", "he"]);

export async function GET(_: Request, { params }: { params: Promise<{ locale: string; slug: string }> }) {
  const { locale, slug } = await params;
  const guide = await getGuide(slug, locale);
  if (!guide) return new Response("not found", { status: 404 });

  const fontFile = FONTS[locale] ?? "Inter-Bold.ttf";
  const font = await fetch(new URL(`../../../../assets/fonts/${fontFile}`, import.meta.url)).then((r) => r.arrayBuffer());

  return new ImageResponse(
    (
      <div style={{ display: "flex", width: "100%", height: "100%", position: "relative", direction: RTL.has(locale) ? "rtl" : "ltr" }}>
        <img src={guide.heroUrl} width={1200} height={630} style={{ position: "absolute", objectFit: "cover" }} />
        <div style={{ display: "flex", flexDirection: "column", justifyContent: "flex-end", padding: 64, width: "100%", background: "linear-gradient(transparent 40%, rgba(0,0,0,.75))", color: "#fff" }}>
          <div style={{ fontSize: 28, opacity: 0.85 }}>{guide.categoryLabel}</div>
          <div style={{ fontSize: guide.title.length > 60 ? 52 : 64, lineHeight: 1.1, fontFamily: "Card" }}>{guide.title}</div>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [{ name: "Card", data: font, weight: 700 }],
      headers: { "Cache-Control": "public, max-age=31536000, immutable" }, // URL carries the version
    },
  );
}

The page’s metadata references the card with the version and sets locale tags.

TypeScript
// in generateMetadata
openGraph: {
  title: guide.title,
  locale: ogLocale(locale),                       // e.g. "fr_FR"
  alternateLocale: translated.filter((l) => l !== locale).map(ogLocale),
  images: [{ url: `${ORIGIN}/og/${locale}/${slug}?v=${guide.revision}`, width: 1200, height: 630, alt: guide.title }],
},
twitter: { card: "summary_large_image" },

Because the URL is immutable per revision, the image can be cached for a year at the CDN and in browsers. A new revision produces a new URL; the old one stays valid for links already shared.

Letting editors override

Some pages deserve a custom card, such as a campaign landing page. Keep the SEO object’s image field as an override: when set, og:image uses it; when empty, the generated card is used. Show the resolved card in the CMS preview so editors see what will be shared before publishing.

Choosing what goes on the card

A card has room for very little, so choose deliberately. The title is almost always worth including, in the reader’s language and large enough to read on a phone in a busy feed. A category or section label helps readers place the content, such as “Travel guide” or “Recipe”. The brand should be present but small, since the platform already shows the domain. A photo from the page makes the card recognizable when readers later land on the page, but it must not compete with the title; a gradient or solid band behind the text keeps contrast high on any photo. Avoid putting dates, prices or other values that change often on generated cards unless the URL’s version changes with them, or shared cards will show stale information for as long as platforms cache them. For products, a clean product image with the name usually outperforms a busy layout with price and badges.

Configuration Reference

Setting Recommendation Why
Size 1200 × 630 PNG Works on major platforms.
URL locale, slug and content revision New content, new URL; long caching.
Cache-Control public, max-age=31536000, immutable Cards never change at a given URL.
Fonts per script, loaded only for its locales Correct glyphs without large bundles.
Text length size down or truncate long titles Avoid overflow in long languages.
Override SEO image field Custom cards for important pages.

Gotchas & Edge Cases

  • Missing glyphs. A Latin font renders Japanese titles as empty boxes. Test every script with real titles.
  • Platform caches. Platforms cache previews per page URL, sometimes for days. Versioned image URLs help only when the platform re-fetches the page; use platform debugging tools to refresh important pages.
  • Remote images in templates. The hero image in the template is fetched during rendering; use a resized rendition, not the original, to keep rendering fast.
  • Absolute URLs. og:image must be absolute, including protocol, or many crawlers ignore it.

Worked Example

The travel company created one card template for guides and one for articles, with fonts for Latin, Japanese and Arabic. Every page’s og:image pointed at the card route with locale and revision, and og:title came from the localized resolver. Social media managers stopped producing cards for routine content and focused on campaigns, where custom designs still made a difference, and used the override field for those pages. Click-through from organic shares in non-English markets rose noticeably in the following quarter, measured by shared-link referral traffic per share.

Referral clicks per organic share by marketAverage referral visits per organically shared link in three non-English markets before and after localized Open Graph cards.fr before3.1 clicks per sharefr after4.4 clicks per shareja before1.8 clicks per shareja after3.5 clicks per sharede before3.4 clicks per sharede after4.6 clicks per share
Localized titles on the card itself made the difference in feeds where most readers do not read English.

Designing Templates That Survive Translation

Card templates meet the same text-expansion problems as any localized design, in a much smaller space. Design for the longest locale: German and Finnish titles run long, and Japanese titles are short in characters but need larger font sizes to stay legible. Use a font-size rule based on title length, as the example does, and a maximum of three lines with an ellipsis as a last resort. Keep brand elements out of the text area so long titles never overlap the logo. For right-to-left locales, mirror the layout, not just the text direction. Review templates with real titles from every locale before launch; a gallery page in the CMS preview that renders the card for the twenty longest titles per locale makes this review quick.

Rollout Checklist

  • Design card templates with space for long and right-to-left titles.
  • Render cards on demand from localized fields, with fonts per script.
  • Reference cards with versioned, absolute URLs and cache them immutably.
  • Set og:locale and og:locale:alternate from translated locales.
  • Keep an editor override for important pages.
  • Test every script and refresh key pages in platform debug tools.

Frequently Asked Questions

Should cards be generated at build time instead?

For small static sites, yes. For large or frequently changing sites, on-demand generation with CDN caching avoids long builds.

Does the card need to include the title if og:title is set?

Platforms show og:title next to the image, but titles in the image stand out in busy feeds. Many sites include both, and test which works better in their own markets.

How much does generation cost?

Each card renders once per revision and locale, then comes from cache. The cost is small compared with page rendering, even for sites with many locales and frequent edits.

What about pages shared before the change?

Platforms keep their cached previews for existing shares. New shares, and re-scraped pages after a refresh in the platform’s debugging tool, pick up the new cards.

Can the same template serve all content types?

Often one template with optional areas is enough. Separate templates help when products need prices or events need dates.