This post was originally published at this blog
Introduction
In this article, we will see how to generate dynamic open graph images. You might be wondering what an open graph image is? Whenever you share a link in Twitter, Discord, or other applications. A fancy card image / link preview is displayed and that image is called an open graph image / OG image.
Requirements
To generate an OG image, we'll be using these npm packages.
Installing Requirements
Do you have yarn? If not, then install it and run the below command or else use npm
yarn add puppeteer-core chrome-aws-lambda
Creating your OG Image
Before creating this function, If you're planning to deploy this project in vercel. I'll suggest everyone create a separate project. Otherwise, we may face this issue π
If we integrate this function with our existing project and deploy it in vercel. There is a chance that we might get an error message that the serverless function dependency package compressed size exceeds 50Mb, and then it stops the deployment process.
To get an idea, we'll see how our final output looks like
From the above Url and image, we understand that the dynamic data are passed as the query parameters to generate an image.
So now we need a function that takes a screenshot of our content and passes it as a response. This can be achieved as follows
import chalk from 'chalk';
import { getContent, getCss } from '../../utils/getContent';
import { getPage } from '../../utils/getPage';
import puppeteer from 'puppeteer-core';
import chrome from 'chrome-aws-lambda';
let page;
const isDev = process.env.NODE_ENV === 'development';
const exePath =
process.platform === 'win32'
? 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
: process.platform === 'linux'
? '/usr/bin/google-chrome'
: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
export const getPage = async () => {
if (page) {
return page;
}
const getOptions = async () => {
let options;
if (isDev) {
options = {
args: [],
executablePath: exePath,
headless: true,
};
} else {
options = {
args: chrome.args,
executablePath: await chrome.executablePath,
headless: chrome.headless,
};
}
return options;
};
const options = await getOptions();
const browser = await puppeteer.launch({
...options,
});
page = await browser.newPage();
return page;
};
export default async function handler(req, res) {
console.info(chalk.cyan('info'), ` - Generating Opengraph images`);
const { title, tags, handle, logo, debug, fontFamily, background, fontFamilyUrl } = req.query;
const css = getCss(fontFamily, fontFamilyUrl, background);
const html = getContent(tags, title, handle, logo, css);
if (debug === 'true') {
res.setHeader('Content-Type', 'text/html');
res.end(html);
return;
}
try {
const page = await getPage();
await page.setViewport({ width: 1200, height: 630 });
await page.setContent(html, { waitUntil: 'networkidle2', timeout: 15000 });
await page.evaluateHandle('document.fonts.ready');
const buffer = await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 1200, height: 630 } });
res.setHeader('Cache-Control', `public, immutable, no-transform, s-maxage=31536000, max-age=31536000`);
res.setHeader('Content-Type', 'image/png');
res.end(buffer);
} catch (error) {
console.error(error);
res.statusCode = 500;
res.setHeader('Content-Type', 'text/html');
res.end('<h1>Internal Error</h1><p>Sorry, there was a problem</p>');
}
}
Let's take a closer look at our code.
// getPage
import puppeteer from 'puppeteer-core';
import chrome from 'chrome-aws-lambda';
let page;
const isDev = process.env.NODE_ENV === 'development';
const exePath =
process.platform === 'win32'
? 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
: process.platform === 'linux'
? '/usr/bin/google-chrome'
: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
export const getPage = async () => {
if (page) {
return page;
}
const getOptions = async () => {
let options;
if (isDev) {
options = {
args: [],
executablePath: exePath,
headless: true,
};
} else {
options = {
args: chrome.args,
executablePath: await chrome.executablePath,
headless: chrome.headless,
};
}
return options;
};
const options = await getOptions();
const browser = await puppeteer.launch({
...options,
});
page = await browser.newPage();
return page;
};
Our getPage function launches the browser and gives us a reference to the browser page. To take a screenshot, we're using the puppeteer-core and chrome-aws-lambda package. If you don't know what puppeteer and chrome-aws-lambda are. Refer to the official docs link in the reference section.
Reference
- https://github.com/puppeteer/puppeteer#puppeteer-core
- https://github.com/alixaxel/chrome-aws-lambda
- https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteer-vs-puppeteer-core
export const getAbsoluteURL = (path) => {
const baseURL = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'http://localhost:3000';
return baseURL + path;
};
// getCss
export const getCss = (fontFamily, fontFamilyUrl, background) => {
return `
${fontFamilyUrl ?? "@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600&display=fallback');"}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: ${
fontFamily ?? 'Nunito'
}, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans','Helvetica Neue', sans-serif;
color: white;
}
.container {
width: 1200px;
height: 630px;
background: ${background ? background : `url(${getAbsoluteURL('/ogbackground.svg')})`};
padding:3rem;
margin:0 auto;
display: flex;
flex-direction:column;
}
.content {
padding: 3rem 5rem;
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
}
.title {
display: flex;
align-items: center;
justify-content: center;
max-width: 840px;
flex: 1;
margin:0 auto;
text-align: center;
}
.title > h1 {
font-size: 64px;
line-height: 74px;
font-weight: 600;
font-style: normal;
}
.logo {
justify-content: space-between;
display: flex;
align-items: center;
padding: 1rem 3rem;
}
.tags {
font-size: 1rem;
display: flex;
gap: 10px;
justify-content: center;
padding: 2rem 0;
}
.pill{
background: #caa8ff33;
color: white;
padding: 0.25rem 1rem;
border-radius: 50rem;
text-transform: capitalize;
box-shadow: 0 0 1rem rgba(0,0,0,0.1);
font-weight: bold;
}
.handle{
font-size: 24px;
font-weight: 600;
}
`;
};
Our getCss function gets all the styles of our og card. It'll take three optional parameters to generate CSS.
// getContent
export const getContent = (tags, title, handle, logo, css) => {
return `
<html>
<meta charset="utf-8">
<title>Generated Image</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
${css}
</style>
<body>
<div class='container'>
<div class="content">
<div class="title"><h1>${title ?? 'Welcome to this site'}</h1></div>
${
tags
? `<div class="tags">
${tags
.split(',')
.map((tag) => {
return `<span key=${tag} class="pill">${tag}</span>`;
})
.join('')}
</div>`
: ''
}
</div>
<div class="logo">
<img src="${logo ?? getAbsoluteURL(`/logo.svg`)}" alt="logo" width="100px" height="100px" >
<div class="handle">${handle ?? '@Jana__Sundar'}</div>
</div>
</div>
</body>
</html>`;
};
Our getContent function generate Html based on the CSS, title, twitter handle, logo, and tags.
Generally, the most recommended dimensions to generate an OG image is 1200 x 630. These are not the perfect values. Check this link to find the different recommendations.
Reference
try {
const page = await getPage();
await page.setViewport({ width: 1200, height: 630 });
await page.setContent(html, { waitUntil: 'networkidle2', timeout: 15000 });
await page.evaluateHandle('document.fonts.ready');
const buffer = await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 1200, height: 630 } });
res.setHeader('Cache-Control', `public, immutable, no-transform, s-maxage=31536000, max-age=31536000`);
res.setHeader('Content-Type', 'image/png');
res.end(buffer);
} catch (error) {
console.error(error);
res.statusCode = 500;
res.setHeader('Content-Type', 'text/html');
res.end('<h1>Internal Error</h1><p>Sorry, there was a problem</p>');
}
Now, we need to pass the generated Html to the set content function then take a screenshot by calling the screenshot function from puppeteer with filetype. Return the buffer value from the screenshot and send it as a response.
Reference
- https://phiilu.com/generate-open-graph-images-for-your-static-next-js-site
- https://github.com/vercel/og-image
This project is open-source on GitHub. Have a closer look if you want.
Hopefully, you have learned how to generate a dynamic OG image. If you have any doubts, you can reach me at mailtojanasundar@gmail.com. Thanks for reading βοΈ and if you enjoyed this article, share it with your friends and colleagues.
Top comments (1)
quite genuine and relevant information you are sharing with us . dua to get someone back in your life