Simple Open Graph Image Generator with AlpineJS and Tailwind CSS

Simple Open Graph Image Generator with AlpineJS and Tailwind CSS

Written by Mithicher Baro on Feb 24th, 2022 Views Report Post

In this post, we'll look at some simple ways to generate open graph images for blog posts or social media sharing.

Some of the features that we will make are as follows:

  • Design a layout with Tailwind CSS
  • Integrate Google Fonts API
  • Text alignment settings tools
  • Download the generated design as image

I'm assuming you're familiar with AlpineJS and Tailwind CSS. Here are a few resources to get you started:

For an OG Image, the recommended pixel dimensions are 1200:630 px.

Design a layout with Tailwind CSS

The layout that we will attempt to create with Tailwind CSS is shown below.

og-banner

The following css gives the result:

<!doctype html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<script src="https://cdn.tailwindcss.com"></script>
	<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>

	<style>
		.pattern-grid {
			background-image: linear-gradient(currentColor 1px, transparent 1px), linear-gradient(to right, currentColor 1px, transparent 1px);
			background-size: 40px 40px;
		}

		[x-cloak] {
			display: none;
		}
	</style>
</head>

<body class="bg-slate-100">

	<div class="relative flex items-top justify-center min-h-screen bg-gray-50 dark:bg-gray-900 sm:items-center py-4 sm:pt-0">
		<div class="max-w-[75rem] mx-auto flex-1">
			<div class="overflow-hidden shadow">
				<div id="banner" style="height: 640px" class="relative flex flex-col shadow bg-white max-w-full">

					<div class="flex-1 flex items-center border-[1em] border-cyan-600 bg-gray-900">
						<div class="relative z-10 px-40 py-6 flex-1">
							<h2 class="font-semibold text-slate-200 text-6xl leading-tight" contenteditable="true"
								spellcheck="false">Simple Open Graph Image Generator with AlpineJS and Tailwind CSS</h2>
							<p class="text-slate-400 font-mono text-xl mt-8" contenteditable="true">Written by @mithicher</p>
						</div>

						<!-- Top Right Grid -->
						<div class="w-64 absolute right-3 top-3">
							<div class="grid grid-cols-6">
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
							</div>
						</div>

						<!-- Bottom Left Grid -->
						<div class="w-64 transform -rotate-180 absolute left-3 bottom-3">
							<div class="grid grid-cols-6">
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
							</div>
						</div>
					</div>

				</div>
			</div>
		</div>
	</div>
	
</body>

</html>

Integrate Google Fonts API & Simple Toolbar Settings

og-toolbar

Visit the link https://developers.google.com/fonts/docs/developer_api and grab a key.

https://www.googleapis.com/webfonts/v1/webfonts?key=YOUR-API-KEY
<div
		x-data="{
      endpoint: 'https://www.googleapis.com/webfonts/v1/webfonts?key=AIzaSyBTZIBr5YQccTE1nPuWvBVIEbdExyzdCrY',
      
      fonts: [],
      
      fontName: 'Space Mono',

      alignment: 'center',
     
      getFontsLists() {
        fetch(this.endpoint)
          .then(response => response.json())
          .then(response => {
            this.fonts = response.items.map((item) => item.family)
          })
      },

      generateFontUrl() {
        return `https://fonts.googleapis.com/css2?family=${this.fontName.replace(' ', '+')}&display=swap`;
      },

      alignText(value) {
			  this.alignment = value 
		  }
    }" 
    x-init="getFontsLists" 
    x-cloak>

</div>
  • x-data: starts an Alpine.js magic
  • x-init: getFontsLists() is used to fetch Google Fonts and store it in x-data's fonts array

x-init is a directive that gets called before the component is processed.

For more details visit here: https://alpinejs.dev/directives/init

We then create a select dropdown to show the lists of fonts

<select x-model="fontName" class="transition duration-150 ease-in-out px-3 h-10 py-2 block text-gray-700 font-sans rounded-lg text-left focus:outline-none focus:border-indigo-500 focus:ring-indigo-500 shadow-sm border sm:text-sm placeholder-gray-400 bg-white disabled:bg-gray-100 border-gray-300">
  <option value="" disabled>Select a font</option>
  <template x-for="(font, fontIndex) in fonts" :key="fontIndex">
    <option :value="font" :selected="fontName === font" x-text="font"></option>
  </template>
</select>
  • x-model="fontName" allows you to bind the value of an select input element to Alpine data named fontName.
  • generateFontUrl() generates a dynamic google font url based on the fontName.

The generated url is then dynamically binded to a link tag.

<link :href="generateFontUrl" rel="stylesheet" crossorigin="anonymous">

Text Alignment Toolbar

<div class="flex items-center shadow-sm border border-gray-300 bg-white rounded-lg px-1 space-x-1.5">
  <button class="block w-8 h-8 hover:bg-indigo-100 rounded-lg" :class="{'bg-indigo-100': alignment === 'center'}" type="button" x-on:click="alignText('center')">Centered Text</button>
  <div class="h-6 border-l"></div>
  <button class="block w-8 h-8 hover:bg-indigo-100 rounded-lg" :class="{'bg-indigo-100': alignment === 'left'}" type="button" x-on:click="alignText('left')">Left Align Text</button>
</div>

Download the generated design as image

Let us now include a javascript library that will help us generates an image from a DOM node using HTML5 canvas and SVG.

https://github.com/bubkoo/html-to-image/

generateImage() {
  htmlToImage.toPng(document.getElementById('banner'))
    .then(function (dataUrl) {
      const link = document.createElement('a')
      link.download = 'og-banner.png'
      link.href = dataUrl
      link.click()
    })
    .catch(function (error) {
      console.error('oops, something went wrong!', error);
    });
}

generateImage() method gets called when we click the download image button.

Full Example

Feel free to try out the full example:

https://gist.github.com/mithicher/de63e925c70b90ba2d6b4eea5cc43e03

<!doctype html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<script src="https://cdn.tailwindcss.com"></script>
	<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>

	<style>
		.pattern-grid {
			background-image: linear-gradient(currentColor 1px, transparent 1px), linear-gradient(to right, currentColor 1px, transparent 1px);
			background-size: 40px 40px;
		}
		[x-cloak] { display: none; }
	</style>
</head>

<body class="bg-slate-100">
	<div class="relative flex items-top justify-center min-h-screen bg-gray-50 dark:bg-gray-900 sm:items-center py-4 sm:pt-0"
		x-data="{
		endpoint: 'https://www.googleapis.com/webfonts/v1/webfonts?key=AIzaSyBTZIBr5YQccTE1nPuWvBVIEbdExyzdCrY',
		fonts: [],
		fontName: 'Space Mono',
		alignment: 'center',
		theme: 'theme1',
		title: 'Simple Open Graph Image Generator with AlpineJS and Tailwind CSS',
		author: 'Written by @mithicher',
		getFontsLists() {
			fetch(this.endpoint)
				.then(response => response.json())
				.then(response => {
					this.fonts = response.items.map((item) => item.family)
				})
		},
		generateFontUrl() {
			return `https://fonts.googleapis.com/css2?family=${this.fontName.replace(' ', '+')}&display=swap`;
		},
		generateImage() {
			htmlToImage.toPng(document.getElementById('banner'))
				.then(function (dataUrl) {
					const link = document.createElement('a')
					link.download = 'og-banner.png'
					link.href = dataUrl
					link.click()
				})
				.catch(function (error) {
					console.error('oops, something went wrong!', error);
				});
		},
		alignText(value) {
			this.alignment = value 
		}
	}" x-init="getFontsLists" x-cloak>
		<link :href="generateFontUrl" rel="stylesheet" crossorigin="anonymous">

		<!-- Settings Toolbar -->
		<div class="fixed top-0 left-0 w-full bg-white flex items-center h-16 shadow">
			<div class="max-w-[75rem] mx-auto flex-1"> 
				<div class="flex space-x-3">
					<select x-model="theme" class="transition duration-150 ease-in-out px-3 h-10 py-2 block text-gray-700 font-sans rounded-lg text-left focus:outline-none focus:border-indigo-500 focus:ring-indigo-500 shadow-sm border sm:text-sm placeholder-gray-400 bg-white disabled:bg-gray-100 border-gray-300">
						<option value="" disabled>Select a theme</option> 
						<option value="theme1" :selected="theme === 'theme1'" x-text="'Theme 1'"></option>
						<option value="theme2" :selected="theme === 'theme2'" x-text="'Theme 2'"></option>
						<option value="theme3" :selected="theme === 'theme3'" x-text="'Theme 3'"></option>
					</select>
					<select x-model="fontName" class="transition duration-150 ease-in-out px-3 h-10 py-2 block text-gray-700 font-sans rounded-lg text-left focus:outline-none focus:border-indigo-500 focus:ring-indigo-500 shadow-sm border sm:text-sm placeholder-gray-400 bg-white disabled:bg-gray-100 border-gray-300">
						<option value="" disabled>Select a font</option>
						<template x-for="(font, fontIndex) in fonts" :key="fontIndex">
							<option :value="font" :selected="fontName === font" x-text="font"></option>
						</template>
					</select>
					<div class="flex items-center shadow-sm border border-gray-300 bg-white rounded-lg px-1 space-x-1.5">
						<button class="block w-8 h-8 hover:bg-indigo-100 rounded-lg" :class="{'bg-indigo-100': alignment === 'center'}" type="button" x-on:click="alignText('center')"><svg
								xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 mx-auto" fill="#6366f1" viewBox="0 0 256 256">
								<rect width="256" height="256" fill="none"></rect>
								<line x1="40" y1="68" x2="216" y2="68" fill="none" stroke="#6366f1" stroke-linecap="round"
									stroke-linejoin="round" stroke-width="16"></line>
								<line x1="64" y1="108" x2="192" y2="108" fill="none" stroke="#6366f1" stroke-linecap="round"
									stroke-linejoin="round" stroke-width="16"></line>
								<line x1="40" y1="148" x2="216" y2="148" fill="none" stroke="#6366f1" stroke-linecap="round"
									stroke-linejoin="round" stroke-width="16"></line>
								<line x1="64" y1="188" x2="192" y2="188" fill="none" stroke="#6366f1" stroke-linecap="round"
									stroke-linejoin="round" stroke-width="16"></line>
							</svg></button>
						<div class="h-6 border-l"></div>
						<button class="block w-8 h-8 hover:bg-indigo-100 rounded-lg" :class="{'bg-indigo-100': alignment === 'left'}" type="button" x-on:click="alignText('left')"><svg
								xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 mx-auto" fill="#6366f1" viewBox="0 0 256 256">
								<rect width="256" height="256" fill="none"></rect>
								<line x1="40" y1="68" x2="216" y2="68" fill="none" stroke="#6366f1" stroke-linecap="round"
									stroke-linejoin="round" stroke-width="16"></line>
								<line x1="40" y1="108" x2="168" y2="108" fill="none" stroke="#6366f1" stroke-linecap="round"
									stroke-linejoin="round" stroke-width="16"></line>
								<line x1="40" y1="148" x2="216" y2="148" fill="none" stroke="#6366f1" stroke-linecap="round"
									stroke-linejoin="round" stroke-width="16"></line>
								<line x1="40" y1="188" x2="168" y2="188" fill="none" stroke="#6366f1" stroke-linecap="round"
									stroke-linejoin="round" stroke-width="16"></line>
							</svg></button>
					</div>
					<div class="flex items-center shadow-sm border border-gray-300 bg-white rounded-lg px-2 space-x-2">
						<button class="flex text-gray-600 pr-2" type="button" x-on:click="generateImage()">
							<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 mr-1" fill="#6366f1" viewBox="0 0 256 256">
								<rect width="256" height="256" fill="none"></rect>
								<path
									d="M40,176V48a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8V160h0l-42.3-42.3a8,8,0,0,0-11.4,0l-44.6,44.6a8,8,0,0,1-11.4,0L85.7,141.7a8,8,0,0,0-11.4,0Z"
									opacity="0.2"></path>
								<rect x="40" y="40" width="176" height="176" rx="8" fill="none" stroke="#6366f1"
									stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></rect>
								<path
									d="M216,160l-42.3-42.3a8,8,0,0,0-11.4,0l-44.6,44.6a8,8,0,0,1-11.4,0L85.7,141.7a8,8,0,0,0-11.4,0L40,176"
									fill="none" stroke="#6366f1" stroke-linecap="round" stroke-linejoin="round"
									stroke-width="16"></path>
								<circle cx="100" cy="92" r="12"></circle>
							</svg>Download image
						</button>
					</div>
				</div>
			</div>
		</div>
		<!-- ./Settings Toolbar -->

		<div class="max-w-[75rem] mx-auto flex-1">
			<div 
				:style="`font-family: ${fontName}, san-serif`" 
				class="overflow-hidden shadow"
				:class="{ 'text-center': `${alignment}` === 'center', 'text-left': `${alignment}` === 'left'}">

				
				<div id="banner" style="height: 640px" class="relative flex flex-col shadow bg-white max-w-full">

					<!-- Banner Style with Tailwind CSS: Theme 1 -->
					<div x-show="theme === 'theme1'" class="flex-1 flex items-center border-[1em] border-cyan-600 bg-gray-900">
						<div class="relative z-10 px-40 py-6 flex-1">
							<h2 class="font-semibold text-slate-200 text-6xl leading-tight" contenteditable="true" spellcheck="false" x-text="title"></h2>
							<p class="text-slate-400 font-mono text-xl mt-8" contenteditable="true" x-text="author"></p>
						</div>
					
						<div class="w-64 absolute right-3 top-3">
							<div class="grid grid-cols-6">
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
							</div>
						</div>

						<div class="w-64 transform -rotate-180 absolute left-3 bottom-3">
							<div class="grid grid-cols-6">
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10"></div>
								<div class="h-10 bg-cyan-600"></div>
							</div>
						</div>
					</div>
					<!-- ./Banner Style with Tailwind CSS: Theme 1 -->

					<!-- Banner Style with Tailwind CSS: Theme 2 -->
					<div x-show="theme === 'theme2'" class="relative flex-1 flex items-center bg-slate-50">
						<div class="-skew-y-12 w-full -mt-12 absolute top-0">
							<div class="grid grid-cols-6">
								<div class="h-12 col-span-1 bg-slate-200"></div>
								<div class="h-12 col-span-5 bg-white"></div>
							</div>
							<div class="grid grid-cols-4">
								<div class="h-12 col-span-1 bg-slate-100"></div>
								<div class="h-12 col-span-2 bg-white"></div>
								<div class="h-12 col-span-1 bg-gray-100"></div>
							</div>
							<div class="grid grid-cols-12">
								<div class="h-12 col-span-1 bg-indigo-600"></div>
								<div class="h-12 col-span-3 bg-indigo-300"></div>
								<div class="h-12 col-span-8 bg-white"></div>
							</div>
							<div class="grid grid-cols-8">
								<div class="h-12 col-span-6 bg-white"></div>
								<div class="h-12 col-span-2 bg-indigo-600"></div>
							</div>
							<div class="grid grid-cols-8">
								<div class="h-12 col-span-7 bg-white"></div>
								<div class="h-12 col-span-1 bg-indigo-300"></div>
							</div>
						</div>
					
						<div class="relative z-40 px-40 py-6">
							<h2 class="font-semibold text-slate-800 text-6xl leading-tight" contenteditable="true" spellcheck="false" x-text="title"></h2>
							<p class="text-slate-600 font-mono text-xl mt-8" contenteditable="true" x-text="author"></p>
						</div>
					
						<div class="-skew-y-12 w-full -mb-20 absolute bottom-0">
							<div class="grid grid-cols-5">
								<div class="h-12 col-span-1 bg-indigo-600"></div>
								<div class="h-12 col-span-4 bg-white"></div>
							</div>
							<div class="grid grid-cols-4">
								<div class="h-12 col-span-1 bg-slate-100"></div>
								<div class="h-12 col-span-2 bg-white"></div>
								<div class="h-12 col-span-1 bg-slate-100"></div>
							</div>
							<div class="grid grid-cols-6">
								<div class="h-12 col-span-1 bg-indigo-600"></div>
								<div class="h-12 col-span-1 bg-indigo-300"></div>
								<div class="h-12 col-span-4 bg-white"></div>
							</div>
							<div class="grid grid-cols-8">
								<div class="h-12 col-span-6 bg-white"></div>
								<div class="h-12 col-span-2 bg-indigo-600"></div>
							</div>
							<div class="grid grid-cols-8">
								<div class="h-12 col-span-7 bg-white"></div>
								<div class="h-12 col-span-1 bg-indigo-300"></div>
							</div>
						</div>
					</div>
					<!-- ./Banner Style with Tailwind CSS: Theme 2 -->

					<!-- Banner Style with Tailwind CSS: Theme 3 -->
					<div x-show="theme === 'theme3'" class="flex-1 flex items-center pattern-grid text-pink-100">
						<div class="h-96 bg-gradient-to-b from-pink-100 via-purple-50 absolute top-0 left-0 right-0"></div>
						<div class="h-96 bg-gradient-to-t from-white absolute bottom-0 left-0 right-0"></div>
					 
						<div class="relative z-10 px-40 py-6">
							<h2 class="font-semibold text-pink-800 text-6xl leading-tight" contenteditable="true" spellcheck="false" x-text="title"></h2>
							<p class="text-slate-600 font-mono text-xl mt-16" contenteditable="true" x-text="author"></p>
						</div>
					</div>
					<!-- ./Banner Style with Tailwind CSS: Theme 3 -->
				</div>
		
			</div>
		</div>
	</div>

	<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/html-to-image.js" crossorigin="anonymous"></script>
</body>

</html>

Conclusion

That's all for now, I hope you enjoyed it. See you next time...Bye 👋

Comments (0)