Creating Dynamic Open Graph images for your blogs!
11/5/2022 •688 views •7 min read
If you are into writing articles or into SEO stuff, you might know about the meta tags.
One of them is the Open Graph meta tag. It is used to display a thumbnail of the article sites like Twitter, Discord and many more.
1
<meta property="og:image" content="http://example.com/image.jpg" />
We’ll need to handle the following:
- Styling
- Font family
- Responsiveness
There are many image manipulation libraries like Jimp or Sharp, but they can’t handle the above points.
You know what can handle it? It’s HTML!
So we will be using Puppeteer that would spin up a headless browser and take a screenshot of the html page.
We will be using the following libraries:
- Express - For spinning up the server
- Puppeteer - For spinning up the browser
- Handlerbars - For rendering the html
- Tailwind CSS - For styling the html
And all this would be deployed on Vercel. This way we could use the Serverless Functions.
Let’s get started.
Here is the file structure:
File Structure
1 2 3 4 5 6 7 8 9 10 11 12 13
. |-- api | `-- index.ts |-- package.json |-- src | `-- templates | |-- basic | | `-- index.hbs | `-- fancy | `-- index.hbs |-- tailwind.config.js |-- vercel.json `-- yarn.lock
Creating the HTML/HBS Template
The HTML code could be anything, that’s up to you. For sake of this blog, we are using a simple template.
I am using Tailwind CDN for styling. Utility Classes FTW!
Head of the HTML
We are importing fonts, and using TailwindCSS CDN for styling.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<!-- templates/basic/index.hbs --> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>{{title}}</title> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Karla&display=swap" rel="stylesheet" /> <script src="https://cdn.tailwindcss.com?plugins=line-clamp"></script> </head> <!-- body ... --> </html>
Body of the HTML
Nothing much here apart from Tailwind CSS classes, just a title and date that handlebars would dynamically update.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<!-- templates/basic/index.hbs --> <body class="image relative bg-black font-bold w-[2240px] h-[1260px] p-32 font-[Karla]" > <h1 class="text-gray-300 text-4xl">SV.</h1> <h1 class="text-white my-32 py-2 leading-[80px] text-6xl line-clamp-3 font-medium" > {{title}} </h1> <h1 class="text-white font-normal my-32 text-3xl line-clamp-3"> {{date}} | by Shubham Verma </h1> <div class="bg-inherit w-full h-full absolute -z-10"></div> <h1 class="text-gray-300 mb-32 absolute bottom-0 text-4xl">shbm.fyi</h1> </body>
Creating the Server and the Handlerbars Template.
Imports
1 2 3 4 5 6 7 8
// api/index.ts import chromium from 'chrome-aws-lambda' // required for deploying on Vercel import express, { Request, Response } from 'express' import { readFileSync } from 'fs' import Handlebars from 'handlebars' import path from 'path' const app = express()
Creating the get Route
Overview of the get route:
- We spin up the puppeteer browser.
- Get the title and date from the query string.
- Compile the hbs template and pass in the title and date.
- Render the html.
- Take a screenshot of the html.
- Close the browser.
- Return the screenshot as a response.
In the very end we are adding this line
module.exports = app;
This is for Vercel to deploy a serverless function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
app.get('/', async (req: Request, res: Response) => { try { // All these configurations are required for puppeteer, if you are deploying on Vercel! // https://github.com/vercel/community/discussions/124 const browser = await chromium.puppeteer.launch({ args: [...chromium.args, '--hide-scrollbars', '--disable-web-security'], defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath, headless: true, ignoreHTTPSErrors: true }) // const browser = await chromium.puppeteer.launch() const [page] = await browser.pages() const { template = 'basic', title, date } = req.query // Reading the template const _template = readFileSync( path.join(process.cwd(), `src/templates/${template}/index.hbs`), 'utf8' ) // Compiling the template const html = Handlebars.compile(_template)({ title, // get date in this format - 21st April 2020 date: date ?? new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }) }) // Rendering the html and taking a screenshot await page.setContent(html) const take = await page.$('.image') const ss = await take.screenshot() await browser.close() // Returning the buffer of the screenshot. res.setHeader('Content-Type', 'image/png') res.setHeader('Cache-Control', 's-max-age=1, stale-while-revalidate') res.send(ss) } catch (err) { console.error(err) res.send('Error') } }) // Required if you are deplying Express on Vercel as a Serverless Function. module.exports = app
Deploying on Vercel
Create a vercel.json and add the following:
1 2 3 4 5 6 7 8 9 10
// vercel.json { "rewrites": [ { "destination": "/api", "source": "/(.*)" } ] }
All set! You can now deploy the server on Vercel.
You can try it out by going to https://og.shubhamverma.me/?title=Hey there .It will take time to load up, because first it’s serverless and second we are spinning up puppeteer.
Please don’t over do it. I’ve got Vercel Serverless Limits 😅
References:
I have Open Sourced the code for this project on Github.
Here is the Repo link - https://github.com/ShubhamVerma1811/open-graph-api