Open graph images for vanilla React apps

May 27, 2020

Mailbrew is a vanilla React app (CRA) so it wasn't as easy as we would have liked to implement open graph tags to get nice social images when people shared their brews on Twitter and messaging apps such as Telegram.

Being client-side rendered app.mailbrew.com can't provide bots that fetch URLs with the proper meta tags for social images to work so we got creative.

We spun up a separate service share.mailbrew.com using serverless function in Vercel and created two endpoints to fix this problem.

Screenshot endpoint

The /screenshot endpoint takes an URL and a size and returns a png image with the screenshot of that page. This takes on average of 10s for our social images, but it's cached at edge (thanks to Vercel) so it only takes 300ms to fetch once it's in cache.

Headless Chrome is used to render the URL and get the png. We adapted the code from this example to our use-case.

Share endpoint

We have another endpoint, backing the actual urls that are being shared: share.mailbrew.com/<username>/<brew>.

It does what the React app can't, serving static HTML with all the meta tags needed for social images to work and then redirect to the actual content at app.mailbrew.com/<username>/<brew> with a script tag.

<html>
  <head>
    <meta property="og:title" content="${title}" />
    <meta property="og:description" content="${description}" />
    <meta property="og:image" content="${image_url}" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:creator" content="@mailbrew" />
    <meta name="twitter:title" content="${title}" />
    <meta name="twitter:description" content="${description}" />
    <meta name="twitter:image" content="${image_url}" />
  </head>
  <body>
    <script>
      window.location.href = "${redirect_url}";
    </script>
  </body>
</html>

In the example code above, image_url uses the screenshot endpoint to serve screenshot of the social card at the desired size (1200x640 in our case), rendered as HTML via our Django backend.

Some final notes

This is a bit convoluted but it works! In the future we will probably transition the project to Next.js and have these pages be server-side rendered to avoid all this madness.


If you go with Vercel, you will need to upgrade to the Pro plan ($20/m) because of the 10s per lambda limit they impose on the hobby plan.

Could have probably have hacked something cheaper with AWS Lambda, API Gateway and Cloudfront, but I didn't want the headache of managing all that.