🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 6 Min Lesezeit
0

Implementing a dynamic OGP image generator for our blog — PHP GD, per-post 1200 630 cards

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

One day on our English X account, I posted a link to a fresh blog article and froze when the OGP card rendered: the image was the LP sales banner — "Stop babysitting updates. Start scaling maintenance revenue." — not anything related to the post itself.



Going back through the last seven announcement posts, every single one was showing the same LP sales banner. Articles written as technical engineering deep-dives had been quietly flowing through the X timeline for months looking like sales-promo posts.



This article walks through how we found the structural cause and replaced it with a dynamic OGP image generation engine — using our own blog as the example.






The structural cause — "no featured image → LP default"



Digging in, the theme's OGP logic was a classic fallback: "if there's a featured image use it, otherwise the LP default." The problem was that none of our 21 articles × 2 languages = 42 posts had a featured image. Every post was returning the same LP sales banner as og:image.




Note: OGP (Open Graph Protocol) is the metadata SNS clients use to render link previews. X, Facebook, LinkedIn, Slack and others all read og:image and friends.







Two options — manual artwork vs dynamic generation



Two repair paths were on the table.



Option A. Manually set a featured image on all 21 posts. Honest answer, but (1) producing 42 images once is heavy, (2) every new article afterward inherits an ongoing artwork cost, and (3) the "Japanese style" stock photos (PCs, keyboards, abstract code) read as amateur to international tech audiences, which works against us



Option B. Build a dynamic OGP image generator. The international tech-blog standard pattern. Vercel, PlanetScale, dev.to and friends all generate black-background + headline-composed OGP cards on demand — render once, cache, and every new post automatically inherits a per-article OGP



We picked Option B:




  • One engine, then 21 posts (and every future post) auto-resolve correctly

  • "Black + title" composition is the visual register international tech readers expect

  • "Find a stock image for every article" stops being a step






The core — ogp-generator.php (PHP GD, ~375 lines)



PHP GD (the image-manipulation library shipped with PHP) renders the 1200×630 PNG on demand. Everything sits in one standalone PHP file, ogp-generator.php.




CODE
// ogp-generator.php (sketch)
$post_id = (int)($_GET['post_id'] ?? 0);
$cache_path = WP_CONTENT_DIR . "/uploads/ogp/post-{$post_id}.png";

// 2nd hit onward: serve from disk cache
if (file_exists($cache_path)) {
header('Content-Type: image/png');
readfile($cache_path);
exit;
}

// First hit: pull title from WP DB
$post = get_post($post_id);
$title = $post->post_title;

// Render 1200×630 PNG via GD
$im = imagecreatetruecolor(1200, 630);

// Background #0d1117 (LP main dark)
$bg = imagecolorallocate($im, 0x0d, 0x11, 0x17);
imagefilledrectangle($im, 0, 0, 1200, 630, $bg);

// Top blue 4px / bottom green 4px (visual continuity with LP)
$blue = imagecolorallocate($im, 0x2f, 0x81, 0xf7);
$green = imagecolorallocate($im, 0x3f, 0xb9, 0x50);
imagefilledrectangle($im, 0, 0, 1200, 4, $blue);
imagefilledrectangle($im, 0, 626, 1200, 630, $green);

// Title text (font picked by language)
$font = has_japanese($title) ? NOTO_BOLD : INTER_BOLD;
$lines = wrap_text($title, $font, 56, 900);
$y = 220;
$white = imagecolorallocate($im, 0xff, 0xff, 0xff);
foreach ($lines as $line) {
imagettftext($im, 56, 0, 100, $y, $white, $font, $line);
$y += 88;
}

// Cache to disk and serve
imagepng($im, $cache_path);
header('Content-Type: image/png');
readfile($cache_path);






Nothing fancy, but the key components are all there. The first request renders; every request after that streams the cached PNG from disk — the GD cost is paid exactly once per post.






Japanese titles need a second font — Inter + Noto Sans JP



The first thing that broke was the fact that Inter has no Japanese glyphs at all. It's a Latin-script-only font; the JIS X 0208 range doesn't exist. Passing a Japanese title through imagettftext() with Inter produces tofu (□□□) or nothing.



The fix: detect whether the title contains Japanese and swap fonts.




CODE
function has_japanese(string $text): bool {
return preg_match('/[ぁ-んァ-ヶ一-龯]/u', $text) === 1;
}






We bundled Noto Sans JP Bold (SIL Open Font License, 5.1 MB) and Inter Bold / Regular (same SIL OFL, ~325 KB each) into the theme assets and rsync them alongside the other theme files.






The functions.php patch — +28 / -2



The OGP logic redirects through the dynamic URL when there's no featured image. The diff is small (+28 / -2), and the existing behavior (posts with a real featured image keep using it) is preserved.




CODE
// After: posts without a featured image return the dynamic URL
function wpmm_blog_og_image($post) {
if (is_singular() && !has_post_thumbnail($post)) {
$cache_bust = date('YmdHis');
return home_url(
"/wp-content/themes/wpmm-blog/ogp-generator.php" .
"?post_id={$post->ID}&v={$cache_bust}"
);
}
// Posts with a featured image still take precedence
$thumb = get_the_post_thumbnail_url($post, 'full');
return $thumb ?: LP_DEFAULT_OGP;
}






&v=YYYYMMDDHHMMSS is a cache-busting query string. When we update the theme and change the OGP design, this forces X and Facebook to re-crawl rather than serving their stale cached image.






Design decisions and the result



The canvas is 1200×630 (OGP standard, X large-image-card recommended). Background #0d1117, accent blue #2f81f7 / green #3fb950 are pulled from the LP brand palette; thin 4px blue (top) / green (bottom) bars give visual continuity with the LP. "WP MAINTENANCE MANAGER" top-left, headline center-left, supporting URL bottom-right. The whole composition is intentionally close to what Vercel's og API and PlanetScale's OGP cards look like — visual signals readers associate with proper engineering blogs.



After deploy, curl -I against all 42 og:image URLs confirmed every one was now serving the dynamic URL. On X TL the visual flipped from "LP sales banner" to "dark card with the post title." Retroactively re-posting old announcements isn't worth it since they're past their relevance window — instead we'll be tracking engagement deltas between "LP-image era" and "dynamic OGP era" as ongoing signal.






Closing — four principles for dynamic OGP



Four takeaways worth keeping from this round:





  1. WP theme OGP defaults can quietly produce "every post has the same image." The classic if (no_thumb) return LP_DEFAULT fallback is well-meaning, but combined with a workflow that doesn't set featured images it becomes one identical sales banner across every post. Audit og:image across all posts periodically — a curl -I sweep of 42 URLs takes seconds


  2. Manual artwork is both a workload tax and a tone hazard. Beyond the ongoing labor, "regional visual signals" matter — Japanese-style stock photography reads as amateur to international tech audiences. Dynamic generation sidesteps both


  3. PHP GD + WP DB + disk cache is a one-file, lightweight stack. Lighter than it sounds. First-render GD pass, then static file serving — per-request load is essentially zero after the first hit. You don't need an edge runtime like Vercel's to do this


  4. Japanese fonts trip you up at the first hour. Latin-only fonts like Inter / Roboto turn Japanese titles into tofu. Build language detection + Noto Sans JP into the path from day one



This article ended up being a meta piece about our own blog's OGP, written for our blog — and naturally this very post now has a dynamic OGP card generated by the engine it describes. Worth checking your own blog's og:image if you're running one — similar regressions can sit under the surface for months without anyone noticing.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Hackers Just Poisoned the Rust Supply Chain | Threat Wire
1 Quelle
Hackers Found a Way Into Humanoid Robots | Threat Wire
1 Quelle
Bits und so #1021 (Passwort für Laufwerk)
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Implementing a dynamic OGP image generator for our blog — PHP GD, per-post 1200 630 cards

Thematisch verwandte Begriffe: Implementing, dynamic, image, generator · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...