Generate dynamic Open Graph images with Laravel

The preface

My colleague @markvaneijk built a nice functionality in his own website to generate a dynamic Open Graph image for each page. The image is automatically generated when a page is shared on social media like Twitter, Facebook or LinkedIn. He decided it would be nice to open source this functionality into a Laravel package I helped him building the package and it's called Laravel Open Graph Image (source code on Github).

How the package works

Showing the actual open graph image requires us to add a meta tag to the webpage which includes a url to the image. The website where you share the page on will get the image from that url and show the image where the url points too. So the only thing we need to do is to create a webpage where the url redirects too and make this webpage dynamic. However, the webpage should be an image. There is a solution to accomplish this but more about this later.

Because we are building this package for Laravel we create a html component. We choose to create a so called anonymous component. The benefit of using an anonymous component is that we can pass as many attributes as we want because we don’t have to define them in the component class. This is exactly what we want because we want the user to be able to decide what attributes they want to use in the template. Someone might only want to show a title while someone else might also want to show a description and an image. We can now add the component in our view file and pass the attributes like this:

<x-open-graph-image::metatags title="Generate Open Graph image" subtitle="Dynamically with Laravel" button="Read more" />

The component renders some metatags based on the attributes we pass through. This is how the component looks like:

@foreach(config('open-graph-image.metatags') as $property => $key)
    @if($attributes->has($key))
    <meta property="{{ $property }}" content="{{ $attributes->get($key) }}">
    @endif
@endforeach
<meta property="og:image" content="{!! url()->signedRoute('open-graph-image.file', collect($attributes)->all()) !!}">
<meta property="og:image:type" content="image/{{ config('open-graph-image.image.extension') }}">
<meta property="og:image:width" content="{{ config('open-graph-image.image.width') }}">
<meta property="og:image:height" content="{{ config('open-graph-image.image.height') }}">

As you can see in the component we refer to a url which leads to our invokable controller. In this controller we need to return the template view with the given attributes and generate an image. To take a screenshot from the template view that we render we use Spatie's package: Browsershot (source code on Github). This package uses headless Chrome by using Puppeteer under the hood. We save this screenshot in a dedicated directory to 'cache' the image so we don't need to generate the image each time but it uses the previous saved image. But how do we know which image to use? The name of the image is based on the signature which is unique because it uses the signed url in combination with the attributes we used. When the url is visited we check if an image with that unique signature already exists. If it does we render that image, if not we create a new image:

  public function __invoke(Request $request)
  {
      if (! app()->environment('local') && ! $request->hasValidSignature()) {
          abort(403);
      }

      $html = View::make('open-graph-image::template', $request->all())
          ->render();

      if ($request->route()->getName() == 'open-graph-image') {
          return $html;
      }

      if (! $this->getStorageFileExists($request->signature)) {
          $this->saveOpenGraphImage($html, $request->signature);
      }

      return $this->getOpenGraphImageResponse($request->signature);
  }

How to use the package

After following the installation guide (which you can find in the docs) it is possible to include the html component into the head of your pageview. The template file which is published during the installation expects at least a title attribute. You can easily edit the template file to your needs. For example if you want to show some extra text inside the template you can add another attribute and access that attribute in the template. Let's take a look at some example code.

The html component:

<x-open-graph-image::metatags title="Vormkracht10" subtitle="Slimme websites" button="Lees meer" />

A part of the template file:

<body class="flex items-center justify-center min-h-screen"> 
   <div class="bg-gray-50 w-[1200px] h-[630px] text-white p-12 border-teal-700 border-[16px] rounded-lg"> 
       <h1 class="font-bold text-6xl text-teal-700 leading-none uppercase">{!! explode(' - ', $title)[0] !!}</h1> 
       @if($description) 
           <h2 class="mt-6 text-4xl font-bold text-gray-500">{{ $description }}</h2> 
       @endif 
       <div class="inline-block px-6 py-3 mt-10 text-[30px] font-bold text-white rounded-lg bg-teal-700">{{ $button }}</div> 
   </div> 
</body> 

When you look into the source code of the page and you copy the url of the meta tag into your browser you will see that it returns a JPEG image:

open-graph-image

Using this package in combination with any JavaScript framework

To make this package actually work with a Javascript framework you have two options:

  1. Use server-side rendering (SSR)
  2. Use Inertia

We only have one of those two options because Javascript apps are rendered within the document <body>. Because of this they are unable to render markup to the document <head> as it's outside of their scope. To help with this you can use SSR or Inertia which ships with an <Head /> component. This component can be used to set the page <title>, <meta> tags and other elements.

In this blog post I will only talk about the second option. An example of how you could pass the open graph meta tag into the Inertia <Head /> component:

  1. Use the helper method in one of your controllers to generate the URL before rendering the component:
og(['title' => 'Generate dynamic Open Graph images', 'subtitle' => 'With Laravel or any Javascript framework']);
  1. Pass the url as props via Inertia to your component and add the meta tag to the <Head /> component:
export default ({ meta }) => {
  return (  
      <main>  
        <Head>  
          {/* ... */} 
          <meta property="og:image" content={meta} />
        </Head>
            
      {/* ... */}
    </main> 
  );
};            

Result

Now when we share this page on any social media platform you will see that the generated image will be shown because of the meta tag. To test these results you can use tools like: