mathieu@laptop:~/Content/PHP$ cat generate-a-static-open-graph-image-for-your-statamic-site.txt
> Title: Generate default Open Graph images for each Entry in your your Statamic site
> Date: June 7, 2026
> Tags: PHP
I've created a simple (and ugly) script to create Open Graph images for each public entry in pure PHP. This can be used in the previous GitHub action when generating the static site.
First add a og:image metatag to your HTML:
<meta property="og:image" content="{{ $page->image ?? $site->image ?? config('app.url') .'/assets/pages/' . ($page->slug ?? $site->slug) . '.png' }}">
Then, after the static site generation, use this snippet in your AppServiceProvider to generate an image for each public entry.
// AppServiceProvider.php
public function boot(): void
{
SSG::after(function () {
// Get all public statamic pages
$pages = Entry::query()
->where('published', true)
->get();
if (!is_dir(config('statamic.ssg.output_path') . '/assets/pages')) {
mkdir(config('statamic.ssg.output_path') . '/assets/pages', 0755, true);
}
$avatar = imagecreatefrompng(resource_path('img/casmo.png'));
$fontPath = resource_path('fonts/SpaceMono-Regular.ttf');
$upscale = 2;
foreach ($pages as $page) {
$image = imagecreatetruecolor(1200 * $upscale, 627 * $upscale);
$bgColor = imagecolorallocate($image, 31, 35, 41);
imagefill($image, 0, 0, $bgColor);
$textColor = imagecolorallocate($image, 255, 255, 255);
$fontSize = 40;
$maxSize = 25;
$dateTop = 50;
if (strlen($page->title ?? '') > 70) {
$fontSize = 20;
$maxSize = 50;
$dateTop = 30;
}
else if (strlen($page->title ?? '') > 50) {
$fontSize = 30;
$maxSize = 40;
$dateTop = 40;
}
$text = $page->title ?? '';
$top = 305;
if (strlen($text) > $maxSize) {
$lines = explode(' ', $text);
$numberOfWords = count($lines);
$add = 0;
if ($numberOfWords % 2 === 0) {
$add = 1;
}
$line1 = implode(' ', array_slice($lines, 0, ceil((count($lines) + $add) / 2)));
$line2 = implode(' ', array_slice($lines, ceil((count($lines) + $add) / 2)));
} else {
$line1 = $text;
$line2 = '';
$top = 335;
}
imagettftext($image, $fontSize * $upscale, 0, 160 * $upscale, $top * $upscale, $textColor, $fontPath, $line1);
if ($line2) {
imagettftext($image, $fontSize * $upscale, 0, 160 * $upscale, ($top+50) * $upscale, $textColor, $fontPath, $line2);
}
// Write date on top of title
$date = $page->date ?? '';
if ($date) {
$date = date('F j, Y', strtotime($date));
$textDateColor = imagecolorallocate($image, 159, 159, 169);
imagettftext($image, 15 * $upscale, 0, 160 * $upscale, ($top-$dateTop) * $upscale, $textDateColor, $fontPath, $date);
}
$tags = join(', ', $page->get('categories') ?? []);
if ($tags) {
$categoryTextColor = imagecolorallocate($image, 124, 207, 0);
imagettftext($image, 15 * $upscale, 0, 160 * $upscale, ($line2 ? $top+80 : $top+30) * $upscale, $categoryTextColor, $fontPath, $tags);
}
imagecopy($image, $avatar, 50 * $upscale, 265 * $upscale, 0, 0, imagesx($avatar), imagesy($avatar));
imagepng($image, config('statamic.ssg.output_path') . '/assets/pages/' . $page->slug . '.png');
}
});
}
You can use the $upscale variable to reduce the blur in LinkedIn posts. Change the font and avatar variables with your own font and image.