Having a personal site is great for standing out and expressing your creativity. But one of the underrated elements of popular sites like medium or substack is the automatic unfurling of content. Luckily there's an easy way to add them to any site.
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@jadenkore" />
<meta property="twitter:domain" content="danielkhoo.io" />
<meta property="twitter:url" content="https://danielkhoo.io/dynamic-open-graph-cards" />
<meta name="twitter:title" content="Twitter and OpenGraph Cards" />
<meta name="twitter:description" content="Having a personal site is great for standing out and expressing your creativity..." />
<meta name="twitter:image" content="https://danielkhoo.io/sd2.png" />
<meta property="og:url" content="https://danielkhoo.io/" />
<meta property="og:type" content="article" />
<meta property="og:title" content="Twitter and OpenGraph Cards" />
<meta property="og:description" content="Having a personal site is great for standing out and expressing your creativity..." />
<meta property="og:image" content="https://danielkhoo.io/sd2.png" />
This is great for showing a static image, but what if you want to dynamically generate the image?
For example, if you want to show a preview of your latest blog post. It would be cool to generate cover images on the fly with specific the title and description like github does.
Luckily, Vercel has a great tool for generating dynamic OG cards called Vercel OG. It leverages Satori to generate images from HTML/CSS. This let's you pull data from the query params.
So instead of a static url image like:
<meta name="twitter:image" content="https://danielkhoo.io/sd2.png" />
The url is a call to an api endpoint with the query params like:
<meta name="twitter:image" content="https://danielkhoo.io/api/og?title=YourTitle&description=YourDescription" />
Implementation is pretty simple, you just need to create a function in the api folder and return the image. The Vercel OG docs have some great examples of how to do this. I've chosen to style my cards similar to Github's
import { ImageResponse } from '@vercel/og'
export const config = {
runtime: 'edge',
}
export default async function handler(req) {
const HOST = 'https://danielkhoo.io'
const { searchParams } = req.nextUrl
const title = searchParams.get('title') || 'danielkhoo.io'
const description = searchParams.get('description') || `Hello there! I'm Daniel. Welcome to my online home for ideas, writing and side projects.`
const robotoArrayBuffer = await fetch(`${HOST}/fonts/Roboto-Regular.ttf`).then(res => res.arrayBuffer())
const robotoBoldArrayBuffer = await fetch(`${HOST}/fonts/Roboto-Bold.ttf`).then(res => res.arrayBuffer())
const truncatedDescription = description.length > 120 ? description.slice(0, 120) + "..." : description
return new ImageResponse(
(
<div
style={{
background: '#fff',
width: '100%',
height: '100%',
padding: 32,
justifyContent: 'center',
alignItems: 'center',
display: 'flex',
}}
>
<div style={{ display: 'flex', flexDirection: 'column', flex: 3, marginRight: 24 }}>
<p style={{ fontSize: 60, fontWeight: 700 }}>{title}</p>
<p style={{ fontSize: 32, color: '#777', lineHeight: '50px' }}>{truncatedDescription}</p>
</div>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt="avatar"
width="256"
src={`${HOST}/dp.jpeg`}
style={{ flex: 1, borderRadius: 32 }}
/>
</div>
),
{
width: 1200,
height: 600,
fonts: [
{
name: 'Roboto',
data: robotoArrayBuffer,
weight: 400,
style: 'normal',
},
{
name: 'Roboto',
data: robotoBoldArrayBuffer,
weight: 700,
style: 'normal',
},
],
}
)
}
Put it all together and you have dynamic image generation with fully customisability. You can easily extend this to multiple images, more elaborate styling, etc.