Generative OpenGraph Images

One of the creators that always stands out on my twitter feed is Seth Godin @ThisIsSethsBlog. Seth himself is not active on Twitter, but, he does use it to drive traffic to his site by retweeting his blog posts.

What stands out though is the use of an alternating photo of him accompanied by the bright yellow/orange (yorange?) color and the simple “”.

OpenGraph image for @thisissethsblog on twitter

On occasion though this image is missing and it made me think, what does their workflow look like for publishing a post? How could we build an OpenGraph image to automate this workflow and, maybe, add more variation?

Default share preview for @thisissethsblog on twitter

Let’s start with the url which will look something like this:

Notice we are passing “The speed of change” in via the title querystring parameter. We are also passing the path of the article so we can link to it with In the future we can use the path parameter to programatically lookup details about the post and add additional creative elements like publish date.

Consistently Random

The first constraint that occured to me is the photo of Seth. It’s easy enough to randomly select an image from a list, however, we don’t want that image to change everytime the opengraph image is generated. The image should be tied to the article for consistency.

To accomplish this we can generate a seed from the text content of the article or title. In javascript, this will look something like:

const generateSeed = (string, defaultSeed = 0) => {
  if (string) {
    // Convert the string to a character array, calculate the character code and return the sum.
    return [...string].map(c => c.charCodeAt(0)).reduce((a, b) => a + b);
  return defaultSeed;

Calling this method with an article title of “The speed of change” will give us a number of 1741.

generateSeed('The speed of change') // => 1741

Now, let’s say we have 10 photos to choose from, we would calculate the “random” photo for this article with a modulo operation 1741 % 10 which gives us 1. Now, as long as we don’t add more photos to choose from, we will always get 1 for the string The speed of change which we can use to get the the item from the photos array… photos[1].

We can use this same method to dynamically change the “blob” mask of the photo.

Putting it all together

Using our getting started template as a base. We can use Tailwindcss and Alpinejs to build our image.

Sample 1

<!doctype html>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src=""></script>
    <script src="" defer></script>
  <body class="h-screen">
    <div class="flex items-center h-full bg-neutral-800">
      <div class="relative w-[1200px] h-[600px] overflow-hidden mx-auto bg-white text-black">

        <div class="flex items-center justify-between w-full h-full p-28" x-data="openGraphImage">
          <div class="relative flex-none w-1/3">
            <div class="aspect-w-1 aspect-h-1"> 
                class="object-cover w-full h-full"
                  '-webkit-mask-image': `url(${shape})`,
                  'mask-image': `url(${shape})`,
                  '-webkit-mask-repeat': 'no-repeat',
                  'mask-repeat': 'no-repeat',
                  '-webkit-mask-size': 'contain',
                  'mask-size': 'contain',
          <div class="flex-none w-2/3 text-right">
            <div class="text-7xl">
              <div x-text="title"></div>


      document.addEventListener('alpine:init', () => {
      'openGraphImage', () => {
          const params = new URLSearchParams(;

          return {
            params: Object.fromEntries(params.entries()),

            get seed() {
              return [...this.title].map(c => c.charCodeAt(0)).reduce((a, b) => a + b);

            get title() {
              return this.params.title || '';

            get path() {
              return this.params.path || '/';

            get shape() {
              const shapes = [

              return shapes[this.seed % shapes.length];

            get photo() {
              const photos = [

              return photos[this.seed % photos.length];

            init() {
              window.__og = window.__og || [];

              // set the redirect url for ``
              window.__og.push(['link', `${this.path}`]);

              // ensure all assets are loaded before rendering
              window.__og.push(['preload', 'fonts', this.shape,]);

              // wait for alpine to finish doing it's thing
              this.$nextTick(() => {
                // let the renderer know we're ready



Create living images and links, dynamically generated and personalized on the fly for embedding on your website or email campaigns.

© 2024 OpenGraphImage, LLC. All rights reserved.