Generate OG images for your Laravel app original

by Freek Van der Herten – 5 minute read

When you share a link on Twitter, Facebook, or LinkedIn, the platform shows a preview image. Getting those Open Graph images right usually means either using an external service or setting up a separate rendering pipeline. We just released laravel-og-image, a package that lets you define your OG image as HTML right inside your Blade views. The package takes a screenshot of that HTML and serves it as the OG image. No external API needed, everything runs on your own server.

Let me walk you through what the package can do.

Getting started

Install the package via Composer:

composer require spatie/laravel-og-image

The package uses spatie/laravel-screenshot under the hood, which requires Node.js and Chrome/Chromium on your server. If you prefer not to install those, you can use Cloudflare's Browser Rendering API instead (more on that later).

The package automatically registers middleware in the web group, so there's no manual configuration needed. Just drop the Blade component into your view:

<x-og-image>
    <div class="w-full h-full bg-blue-900 text-white flex items-center justify-center">
        <h1 class="text-6xl font-bold">{{ $post->title }}</h1>
    </div>
</x-og-image>

That's all you need. The component outputs a hidden <template> tag in the page body, and the middleware injects the og:image, twitter:image, and twitter:card meta tags into the <head>:

<head>
    <!-- your existing head content -->
    <meta property="og:image" content="https://yourapp.com/og-image/a1b2c3d4e5f6.jpeg">
    <meta name="twitter:image" content="https://yourapp.com/og-image/a1b2c3d4e5f6.jpeg">
    <meta name="twitter:card" content="summary_large_image">
</head>

The image URL contains a hash of the HTML content. When you change the template, the hash changes, so crawlers automatically pick up the new image.

How it works

The clever bit is that your OG image template lives on the actual page, so it inherits your page's existing CSS, fonts, and Vite assets. No separate stylesheet configuration needed.

Here's what happens when a crawler requests the image:

  1. The request hits the package's controller at /og-image/{hash}.jpeg
  2. The controller looks up the original page URL from cache (stored there by the Blade component during rendering)
  3. Chrome visits that page with ?ogimage appended
  4. The middleware detects the ?ogimage parameter and replaces the response with a minimal HTML page: just the <head> (preserving all CSS and fonts) and the template content at 1200x630 pixels
  5. Chrome takes a screenshot and saves it to disk
  6. The image is served back to the crawler with Cache-Control headers

Subsequent requests serve the image directly from disk. The route runs without sessions, CSRF, or cookies, and the content-hashed URLs play nicely with CDNs like Cloudflare.

You can preview any OG image by appending ?ogimage to the page URL. This is really useful while designing your templates.

Using a Blade view

Instead of writing the HTML inline, you can reference a separate Blade view:

<x-og-image
    view="og-image.post"
    :data="['title' => $post->title, 'author' => $post->author->name]"
/>

The view receives the data array as variables:

{{-- resources/views/og-image/post.blade.php --}}
<div class="w-full h-full bg-blue-900 text-white flex items-center justify-center p-16">
    <div>
        <h1 class="text-6xl font-bold">{{ $title }}</h1>
        <p class="text-2xl mt-4">by {{ $author }}</p>
    </div>
</div>

This is handy when you reuse the same layout across multiple pages or when the template gets complex enough that you want it in its own file.

Fallback images

Pages that don't use the <x-og-image> component won't get any OG image meta tags by default. You can register a fallback in your AppServiceProvider:

use Illuminate\Http\Request;
use Spatie\OgImage\Facades\OgImage;

public function boot(): void
{
    OgImage::fallbackUsing(function (Request $request) {
        return view('og-image.fallback', [
            'title' => config('app.name'),
            'url' => $request->url(),
        ]);
    });
}

The closure receives the full Request object, so you can use route parameters and model bindings to customize the image. Return null to skip the fallback for specific requests. Pages that do have an explicit <x-og-image> component are never affected by the fallback.

Customizing screenshots

You can configure the image size, format, quality, and storage disk via the OgImage facade in your AppServiceProvider:

use Spatie\OgImage\Facades\OgImage;

OgImage::format('webp')
    ->size(1200, 630)
    ->disk('s3', 'og-images');

By default, images are generated at 1200x630 with a device scale factor of 2, resulting in crisp 2400x1260 pixel images. You can also override the size per component:

<x-og-image :width="800" :height="400">
    <div>Custom size OG image</div>
</x-og-image>

If you don't want to install Node.js and Chrome on your server, you can use Cloudflare's Browser Rendering API instead:

OgImage::useCloudflare(
    apiToken: env('CLOUDFLARE_API_TOKEN'),
    accountId: env('CLOUDFLARE_ACCOUNT_ID'),
);

Pre-generating images

By default, images are generated lazily on the first crawler request. If you'd rather have them ready ahead of time, you can pre-generate them with an artisan command:

php artisan og-image:generate https://yourapp.com/page1 https://yourapp.com/page2

Or programmatically, which is useful for generating the image right after publishing content:

use Spatie\OgImage\Facades\OgImage;

class PublishPostAction
{
    public function execute(Post $post): void
    {
        // ... publish logic ...

        dispatch(function () use ($post) {
            OgImage::generateForUrl($post->url);
        });
    }
}

In closing

Our og image package is already running on the blog you're reading right now. You can see the pull request that added it to freek.dev if you want a real-world example of how to integrate it. Try appending ?ogimage to the URL of any post on this blog to see which image would be generated for that post.

With this package, your OG images are just Blade views. You design them with the same Tailwind classes, fonts, and assets you already use in the rest of your app. No separate rendering setup, no external API, no manual meta tag management.

You can find the full documentation on our documentation site and the source code on GitHub.

The approach of using a <template> tag to define OG images inline with the page's own CSS is inspired by OGKit by Peter Suhm. If you'd rather not self-host the generation of OG images, definitely check out OGKit.

This is one of the many packages we have created at Spatie. If you want to support our open source work, consider picking up one of our paid products.

Join 9,500+ smart developers

Get my monthly newsletter with what I learn from running Spatie, building Oh Dear, and maintaining 300+ open source packages. Practical takes on Laravel, PHP, and AI that you can actually use.

"Always fresh, useful tips and articles. Carefully selected community content. My favorite newsletter, which I look forward to every time."

Bert De Swaef — Developer at Vulpo & Youtuber at Code with Burt

No spam. Unsubscribe anytime. You can also follow me on X.

Found something interesting to share? Submit a link to the community section.