Create beautiful Open Graph images with Browsershot and Tailwind CSS

For Artisan School, I wanted to create custom Open Graph images for each video page. For example, when somebody shares a link on social media, platforms like Facebook and Twitter use the OG image to represent the shared link. There are many services to create and serve OG images, but I didn't want to depend on an external service. For the website, I already had a Laravel application up and running with Tailwind CSS.

The routes file for this app is quite simple. Based on the slug URI segment of the route, I fetch the video from the database and return a Blade view:

use App\Models\Video;
 
Route::get('/{slug}', function ($slug) {
$video = Video::whereSlug($slug)->firstOrFail();
 
return view('video', ['video' => $video]);
});

I added a second route, which also returns a Blade view, just to render the OG image.

Route::get('/{slug}/openGraphImage', function ($slug) {
$video = Video::whereSlug($slug)->firstOrFail();
 
return view('videoOpenGraphImage', ['video' => $video]);
})->name('video.openGraphImage');

These images are typically 1200x630 pixels, so I created a container with those dimensions and centered it on the page. Be sure to enable Tailwind's JIT mode to get the square-bracket notation to work. In this container, I added the Artisan School logo and the title of the video.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
</head>
<body class="flex items-center justify-center min-h-screen">
<div class="bg-blue-500 w-[1200px] h-[630px]">
<!-- your content -->
</div>
</body>
</html>

Now how do we store an image out of this Blade view? There's a gorgeous package called Browsershot by Spatie. It converts a webpage to an image or PDF using a headless version of Google Chrome. It needs some setup, but it's straightforward to use. I added a method within my Video model that handles the conversion and saves the image to the public folder of my app.

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Spatie\Browsershot\Browsershot;
 
class Video extends Model
{
public function saveOpenGraphImage()
{
$path = Storage::disk('public')->path("{$this->slug}.jpg");
 
Browsershot::url(route('video.openGraphImage', ['slug' => $this->slug]))
->waitUntilNetworkIdle()
->showBackground()
->windowSize(1200, 630)
->setScreenshotType('jpeg', 100)
->save($path);
}
 
public function getOpenGraphImageUrl(): string
{
return Storage::disk('public')->url("{$this->slug}.jpg");
}
}

To make my life easy, I also added a method to return the full URL of the OG image. You can use this in the header section of the HTML page or with an SEO package like artesaos/seotools.

<meta property="og:image" content="{{ $video->getOpenGraphImageUrl() }}">