Auto Generate Open Graph Images With Laravel
Tags: laravel, php, webdev, beginners
Published: 2024-05-28 08:15:20

Laravel is a popular PHP framework that makes it easy to build web applications. In this article, we will look at how to auto-generate Open Graph images with Laravel.

For this functionality we're going to generate the image in the background, cache this image in the local storage and then serve this image to the user. This way we can generate the image once and then serve it to the user without having to generate it every time.

In order to do this we're going to use the package `spatie/browsershot` that will use the headless browser to generate the image. This will allow us to create a route that the headless browser to use to store the output as a image.

## Install Browsershot
The first thing you need to do is install the `spatie/browsershot` package. This package is a utility for generating images using a headless browser. You can install it using composer.

```bash
composer require spatie/browsershot
```

## Create The Route For The Image
Once you have installed the `spatie/browsershot` package, you can create a new route that will generate the Open Graph image. This route will use the `browsershot` package to generate the image and store it in the local storage.

Here is an example of how you can create a route that generates an Open Graph image:

```php
Route::get('/og-image', OgImageController::class);
```

In this example, we are creating a new route that will use the `OgImageController` to generate the Open Graph image.

## Create The Controller
There's a few steps for this controller so we're going to break it down into a few steps.

First we need to create the controller with the invoke method that will generate the image.

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class OgImageController extends Controller
{
    public function __invoke(Request $request)
    {

    }
}
```

We're going to pass in a querystring into the route that we'll use as the title in the generated image. This will make the route look like this:

```bash
/og-image?title=Hello%20World
```

In the controller we need to access this querystring to use as the text in the image we do this by using the `$request` object.

```php
$request->validate([
    'title' => 'required|string',
]);

$title = $request->get('title');
```

## Generate The HTML From Blade
We need to get the HTML from the blade file as we're going to pass this into the browsershot package, we don't want to return the HTML in the controller but we need render the HTML.

```php
$html = view('ogimage', [
    'title' => $title,
])->render();
```

Create the blade file in the resources/views directory called `ogimage.blade.php` with the following content:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    @vite('resources/js/app.js')
</head>
<body>
<div class="border-2 border-gray-800 w-[1200px] h-[630px] bg-gray-900">
    <div class="flex flex-col items-center justify-center h-full w-full bg-cover bg-center bg-no-repeat">
        <h1 class="font-bold text-4xl text-white">{{ $title }}</h1>
    </div>
</div>
</body>
</html>
```

Notice the `@vite` directive this is a custom directive that will include the Vite assets in the blade file. You will need to either use your main CSS file or in this case we're using Javascript to import the CSS. Use the one that fits your application.

## Generate The Image
Back in the controller we can now generate the image using the `browsershot` package. We're going to use the `browsershot` package to generate the image and store it in the local storage.

```php
$slugTitle = Str::slug($title);

$browsershot = Browsershot::html($html)
    ->noSandbox()
    ->showBackground()
    ->windowSize(1200, 630)
    ->setScreenshotType('png');

if (config('ogimage.node_path')) {
    $browsershot->setNodeBinary(config('chrome.node_path'));
}

if (config('ogimage.npm_path')) {
    $browsershot->setNpmBinary(config('chrome.npm_path'));
}

$image = $browsershot->screenshot();

// Store image locally
Storage::disk('local')->put('public/og-images/' . $slugTitle . '.png', $image);
```

In this example, we take the generated HTML and pass it into the Browsershot package. We set the window size to `1200x630` pixels and set the screenshot type to PNG. We then store the image in the local storage.

## Serve The Image
Now that we have generated the image and stored it in the local storage, we can serve this image to the user. We can do this by returning the image from the controller.

```
return response($image, 200, [
    'Content-Type' => 'image/png',
]);
```

## Checking For Existing Image
The above code will generate the image everytime and store it in the local storage, but we don't need to generate the image everytime. We can check if the image exists and if it does we can return the image from the local storage.

```php
if (Storage::disk('local')->exists('public/og-images/'.$slugTitle.'.png')) {
    return response(Storage::disk('local')->get('public/og-images/'.$slugTitle.'.png'), 200, [
        'Content-Type' => 'image/png',
    ]);
}
```

We can put this code at the top of the controller and if the image exists we can return the image from the local storage.

## Full Controller Code
Below is the full code used in the controller that you can use to generate the ogimage

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Spatie\Browsershot\Browsershot;

class OgImageController extends Controller
{
    public function __invoke(Request $request)
    {
        $request->validate([
            'title' => 'required|string',
        ]);

        $title = $request->get('title');
        $slugTitle = Str::slug($title);

        if ($this->hasImage($slugTitle)) {
            return response($this->getImage($slugTitle), 200, [
                'Content-Type' => 'image/png',
            ]);
        }

        $html = view('ogimage', [
            'title' => $title,
        ])->render();

        $browsershot = Browsershot::html($html)
            ->noSandbox()
            ->showBackground()
            ->windowSize(1200, 630)
            ->setScreenshotType('png');

        if (config('chrome.node_path')) {
            $browsershot->setNodeBinary(config('chrome.node_path'));
        }

        if (config('chrome.npm_path')) {
            $browsershot->setNpmBinary(config('chrome.npm_path'));
        }

        $image = $browsershot->screenshot();

        $this->saveImage($slugTitle, $image);

        return response($image, 200, [
            'Content-Type' => 'image/png',
        ]);
    }

    private function getFilePath($slugTitle)
    {
        return 'public/og-images/'.$slugTitle.'.png';
    }

    private function hasImage($slugTitle)
    {
        return Storage::disk('local')->exists($this->getFilePath($slugTitle));
    }

    private function getImage($slugTitle)
    {
        return Storage::disk('local')->get($this->getFilePath($slugTitle));
    }

    private function saveImage($slugTitle, $image)
    {
        Storage::disk('local')->put($this->getFilePath($slugTitle), $image);
    }
}
```

## Add Meta Tags To Your Site
In order to use this image when posting to social media we need to add the Open Graph meta tags to the site. This will tell the social media platform to use the image we generated.

```html
<meta property="og:title" content="Hello World">
<meta property="og:image" content="/og-image?title=Hello%20World">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
```

In this example, we are setting the title of the post to "Hello World" and the image to the route we created earlier.

## Conclusion
In this article, we looked at how to auto-generate Open Graph images with Laravel. We used the `spatie/browsershot` package to generate the image and store it in the local storage. This allows us to generate the image once and then serve it to the user without having to generate it every time.

This functionality can be useful for generating Open Graph images for blog posts, social media posts, and other content that requires an image.